///|
// Google Gemini Interactions API — MoonBit JS-backend bindings.
//
// Only the Interactions API (ai.google.dev/api/interactions-api) is covered;
// the classic generateContent / Chat / Live APIs are intentionally out of scope.
//
// Requires the Node.js (JS) build target:
// moon build --target js
//
// Depends on the `@google/genai` npm package (install with yarn / npm / pnpm).
// ---------------------------------------------------------------------------
// ── Opaque JS runtime handles ───────────────────────────────────────────────
///|
/// Opaque handle to the native `GoogleGenAI` JS object.
#external
pub(all) type JSGoogleGenAI
///|
/// Opaque handle to a JavaScript `Promise`.
///
/// The type parameter `A` is phantom — it tracks what type the promise
/// resolves to in MoonBit's type system, but carries no runtime information.
#external
pub(all) type JSPromise[A]
// ── Error type ──────────────────────────────────────────────────────────────
///|
/// Error raised when parsing or decoding fails.
pub(all) suberror GenaiError {
ParseFailed(String)
MissingField(String)
InvalidFormat(String)
} derive(Show)
// ── InteractionStatus ───────────────────────────────────────────────────────
///|
/// Possible states of an [`Interaction`] returned by the API.
pub(all) enum InteractionStatus {
/// The interaction is still being generated.
InProgress
/// The interaction is waiting for a client-provided tool result.
RequiresAction
/// The interaction finished successfully.
Completed
Failed
Cancelled
Incomplete
} derive(Show, Eq)
// ── Content block types ─────────────────────────────────────────────────────
///|
/// An annotation (citation) attached to a [`TextContent`] block.
pub(all) struct Annotation {
source : String?
start_index : Int?
end_index : Int?
} derive(Show)
///|
/// A text content block (`type: "text"`).
pub(all) struct TextContent {
text : String?
annotations : Array[Annotation]?
} derive(Show)
///|
/// An image content block (`type: "image"`).
///
/// Provide either `data` (base-64) together with `mime_type`, or a `uri`.
pub(all) struct ImageContent {
data : String?
mime_type : String?
/// `"low"` | `"medium"` | `"high"` | `"ultra_high"`
resolution : String?
uri : String?
} derive(Show)
///|
/// An audio content block (`type: "audio"`).
///
/// Provide either `data` (base-64) together with `mime_type`, or a `uri`.
pub(all) struct AudioContent {
data : String?
mime_type : String?
uri : String?
} derive(Show)
///|
/// A document content block (`type: "document"`).
/// **Untested** — not yet verified at runtime.
pub(all) struct DocumentContent {
data : String?
mime_type : String?
uri : String?
} derive(Show)
///|
/// A video content block (`type: "video"`).
/// **Untested** — not yet verified at runtime.
pub(all) struct VideoContent {
data : String?
mime_type : String?
resolution : String?
uri : String?
} derive(Show)
///|
/// A thought content block (`type: "thought"`).
/// **Untested** — not yet verified at runtime.
pub(all) struct ThoughtContent {
signature : String?
/// Summary content as raw JSON (array of text/image content blocks).
summary : Json?
} derive(Show)
///|
/// A function-call content block (`type: "function_call"`).
pub(all) struct FunctionCallContent {
id : String
name : String
arguments : Json
} derive(Show)
///|
/// A function-result content block (`type: "function_result"`).
pub(all) struct FunctionResultContent {
call_id : String
result : Json
name : String?
is_error : Bool?
} derive(Show)
///|
/// A code-execution call content block (`type: "code_execution_call"`).
/// **Untested** — not yet verified at runtime.
pub(all) struct CodeExecutionCallContent {
id : String
/// Arguments as JSON (`{ "code": "...", "language": "..." }`).
arguments : Json
} derive(Show)
///|
/// A code-execution result content block (`type: "code_execution_result"`).
/// **Untested** — not yet verified at runtime.
pub(all) struct CodeExecutionResultContent {
id : String
output : String
} derive(Show)
///|
/// A Google Search call content block (`type: "google_search_call"`).
/// **Untested** — not yet verified at runtime.
pub(all) struct GoogleSearchCallContent {
id : String
/// Arguments as JSON (`{ "query": "..." }`).
arguments : Json
} derive(Show)
///|
/// A Google Search result content block (`type: "google_search_result"`).
/// **Untested** — not yet verified at runtime.
pub(all) struct GoogleSearchResultContent {
id : String
/// Array of search results as JSON.
result : Json
} derive(Show)
///|
/// A URL-context call content block (`type: "url_context_call"`).
/// **Untested** — not yet verified at runtime.
pub(all) struct URLContextCallContent {
id : String
arguments : Json
} derive(Show)
///|
/// A URL-context result content block (`type: "url_context_result"`).
/// **Untested** — not yet verified at runtime.
pub(all) struct URLContextResultContent {
id : String
result : Json
} derive(Show)
///|
/// An MCP server tool call content block (`type: "mcp_server_tool_call"`).
/// **Untested** — not yet verified at runtime.
pub(all) struct MCPServerToolCallContent {
id : String
name : String
arguments : Json
} derive(Show)
///|
/// An MCP server tool result content block (`type: "mcp_server_tool_result"`).
/// **Untested** — not yet verified at runtime.
pub(all) struct MCPServerToolResultContent {
id : String
result : Json
} derive(Show)
///|
/// A file-search call content block (`type: "file_search_call"`).
/// **Untested** — not yet verified at runtime.
pub(all) struct FileSearchCallContent {
id : String
arguments : Json
} derive(Show)
///|
/// A file-search result content block (`type: "file_search_result"`).
/// **Untested** — not yet verified at runtime.
pub(all) struct FileSearchResultContent {
id : String
result : Json
} derive(Show)
///|
/// Discriminated union of all content-block variants produced by / sent to
/// the model via the Interactions API.
pub(all) enum Content {
Text(TextContent)
Image(ImageContent)
Audio(AudioContent)
/// **Untested** — not yet verified at runtime.
Document(DocumentContent)
/// **Untested** — not yet verified at runtime.
Video(VideoContent)
/// **Untested** — not yet verified at runtime.
Thought(ThoughtContent)
FunctionCall(FunctionCallContent)
FunctionResult(FunctionResultContent)
/// **Untested** — not yet verified at runtime.
CodeExecutionCall(CodeExecutionCallContent)
/// **Untested** — not yet verified at runtime.
CodeExecutionResult(CodeExecutionResultContent)
/// **Untested** — not yet verified at runtime.
GoogleSearchCall(GoogleSearchCallContent)
/// **Untested** — not yet verified at runtime.
GoogleSearchResult(GoogleSearchResultContent)
/// **Untested** — not yet verified at runtime.
URLContextCall(URLContextCallContent)
/// **Untested** — not yet verified at runtime.
URLContextResult(URLContextResultContent)
/// **Untested** — not yet verified at runtime.
MCPServerToolCall(MCPServerToolCallContent)
/// **Untested** — not yet verified at runtime.
MCPServerToolResult(MCPServerToolResultContent)
/// **Untested** — not yet verified at runtime.
FileSearchCall(FileSearchCallContent)
/// **Untested** — not yet verified at runtime.
FileSearchResult(FileSearchResultContent)
} derive(Show)
// ── Turn ────────────────────────────────────────────────────────────────────
///|
/// Content within a [`Turn`] — either a plain string or structured blocks.
pub(all) enum TurnContent {
Plain(String)
Parts(Array[Content])
} derive(Show)
///|
/// A single conversation turn (user **or** model).
pub(all) struct Turn {
content : TurnContent?
/// `"user"` or `"model"`
role : String?
} derive(Show)
// ── Input ───────────────────────────────────────────────────────────────────
///|
/// Input accepted by [`GoogleGenAI::interactions_create`].
///
/// - `Plain(text)` — a single text string.
/// - `Parts(contents)` — one or more typed content blocks.
/// - `Turns(turns)` — a full conversation history (for stateless multi-turn).
pub(all) enum Input {
Plain(String)
Parts(Array[Content])
Turns(Array[Turn])
} derive(Show)
// ── Usage ───────────────────────────────────────────────────────────────────
///|
/// Token-usage counters attached to a completed [`Interaction`].
pub(all) struct Usage {
total_input_tokens : Int?
total_output_tokens : Int?
total_cached_tokens : Int?
} derive(Show)
// ── Tool declarations ────────────────────────────────────────────────────────
///|
/// A `function`-type tool declaration.
pub(all) struct FunctionTool {
name : String
description : String?
/// JSON Schema object describing the function's parameters.
parameters : Json?
} derive(Show)
///|
/// Configuration for the Computer-Use tool.
/// **Untested** — not yet verified at runtime.
pub(all) struct ComputerUseTool {
/// The environment being operated (`"browser"`).
environment : String?
excluded_predefined_functions : Array[String]?
} derive(Show)
///|
/// Configuration for a remote MCP server tool.
/// **Untested** — not yet verified at runtime.
pub(all) struct MCPServerTool {
/// The full URL for the MCP server endpoint.
url : String?
name : String?
/// Authentication / custom headers as JSON object.
headers : Json?
/// Allowed tools configuration as JSON.
allowed_tools : Json?
} derive(Show)
///|
/// Configuration for the File Search tool.
/// **Untested** — not yet verified at runtime.
pub(all) struct FileSearchTool {
file_search_store_names : Array[String]?
top_k : Int?
metadata_filter : String?
} derive(Show)
///|
/// Built-in tool types that the model can use.
pub(all) enum Tool {
/// A developer-defined function the model can invoke.
Function(FunctionTool)
/// Allow the model to search Google.
GoogleSearch
/// Allow the model to execute code.
CodeExecution
/// Allow the model to fetch and read URLs.
URLContext
/// Allow the model to interact with the computer.
/// **Untested** — not yet verified at runtime.
ComputerUse(ComputerUseTool)
/// A remote MCP server that the model can call.
/// **Untested** — not yet verified at runtime.
MCPServer(MCPServerTool)
/// Allow the model to search files.
/// **Untested** — not yet verified at runtime.
FileSearch(FileSearchTool)
} derive(Show)
// ── GenerationConfig ─────────────────────────────────────────────────────────
///|
/// A voice+language pair for speech output.
pub(all) struct SpeechConfig {
voice : String?
language : String?
/// Speaker name for multi-speaker TTS.
/// **Untested** — not yet verified at runtime.
speaker : String?
} derive(Show)
///|
/// Configuration for image generation.
/// **Untested** — not yet verified at runtime.
pub(all) struct ImageConfig {
/// `"1:1"` | `"2:3"` | `"3:2"` | `"3:4"` | `"4:3"` | `"4:5"` | `"5:4"` | `"9:16"` | `"16:9"` | `"21:9"`
aspect_ratio : String?
/// `"1K"` | `"2K"` | `"4K"`
image_size : String?
} derive(Show)
///|
/// Optional generation configuration for an interaction.
pub(all) struct GenerationConfig {
max_output_tokens : Int?
temperature : Double?
top_p : Double?
seed : Int?
stop_sequences : Array[String]?
/// `"minimal"` | `"low"` | `"medium"` | `"high"`
thinking_level : String?
/// `"auto"` | `"none"`
thinking_summaries : String?
speech_config : Array[SpeechConfig]?
/// Configuration for image generation.
/// **Untested** — not yet verified at runtime.
image_config : ImageConfig?
/// Tool choice configuration as JSON.
/// **Untested** — not yet verified at runtime.
tool_choice : Json?
} derive(Show)
// ── CreateParams ─────────────────────────────────────────────────────────────
///|
/// Parameters for `interactions.create`.
pub(all) struct CreateParams {
/// The Gemini model to use, e.g. `"gemini-2.5-flash"`.
model : String
/// Input sent to the model.
input : Input
/// ID of the previous interaction for stateful conversation threads.
previous_interaction_id : String?
generation_config : GenerationConfig?
tools : Array[Tool]?
/// Requested response modalities: `"text"`, `"image"`, `"audio"`.
response_modalities : Array[String]?
/// Enforce a JSON response format (pass a JSON Schema as `Json`).
response_format : Json?
response_mime_type : String?
system_instruction : String?
/// Whether to persist this interaction server-side. Default: `true`.
store : Bool?
/// Run the interaction in the background (for agents / deep-research).
/// **Untested** — not yet verified at runtime.
background : Bool?
/// Whether to stream the response. Use `interactions_create_stream` instead.
/// **Untested** — not yet verified at runtime.
stream : Bool?
} derive(Show)
// ── Interaction (response) ───────────────────────────────────────────────────
///|
/// The Interaction resource returned by the API.
pub(all) struct Interaction {
/// Unique identifier for this interaction.
id : String
status : InteractionStatus
model : String?
/// The model's response content blocks.
outputs : Array[Content]?
previous_interaction_id : String?
role : String?
usage : Usage?
/// ISO 8601 creation timestamp.
created : String?
/// ISO 8601 last-update timestamp.
updated : String?
/// The name of the agent, if an agent was used.
/// **Untested** — not yet verified at runtime.
agent : String?
} derive(Show)
// ── GoogleGenAI client ───────────────────────────────────────────────────────
///|
/// A `GoogleGenAI` client wrapping the `@google/genai` JS SDK.
///
/// Only the Interactions API surface is exposed here. Example:
///
/// ```moonbit nocheck
/// let ai = GoogleGenAI::new("YOUR_GEMINI_API_KEY")
///
/// let p = ai.interactions_create({
/// model: "gemini-2.5-flash",
/// input: Input::Plain("Hello!"),
/// previous_interaction_id: None,
/// generation_config: None,
/// tools: None,
/// response_modalities: None,
/// response_format: None,
/// response_mime_type: None,
/// system_instruction: None,
/// store: None,
/// })
/// // `p` is `JSPromise[String]` — resolves to a JSON-encoded Interaction
/// ```
pub(all) struct GoogleGenAI {
priv js : JSGoogleGenAI
}
// ── Low-level JS FFI ─────────────────────────────────────────────────────────
///|
extern "js" fn ffi_new_client(api_key : String) -> JSGoogleGenAI =
#| (apiKey) => { const {GoogleGenAI} = require('@google/genai'); return new GoogleGenAI({apiKey}); }
///|
extern "js" fn ffi_new_client_with_base_url(
api_key : String,
base_url : String,
) -> JSGoogleGenAI =
#| (apiKey, baseUrl) => {
#| const { GoogleGenAI } = require('@google/genai')
#| return new GoogleGenAI({ apiKey, httpOptions: { baseUrl } })
#| }
///|
/// Calls `client.interactions.create(params)` and serialises the resolved
/// `Interaction` to a JSON string.
extern "js" fn ffi_interactions_create(
client : JSGoogleGenAI,
params_json : String,
) -> JSPromise[String] =
#| (client, p) => {
#| const strip = v => {
#| if (Array.isArray(v)) return v.map(strip)
#| if (v !== null && typeof v === 'object')
#| return Object.fromEntries(
#| Object.entries(v)
#| .filter(([, x]) => x !== null)
#| .map(([k, x]) => [k, strip(x)])
#| )
#| return v
#| }
#| return client.interactions.create(strip(JSON.parse(p)))
#| .then(r => JSON.stringify(r))
#| }
///|
/// Calls `client.interactions.get(id)` and serialises the result.
extern "js" fn ffi_interactions_get(
client : JSGoogleGenAI,
id : String,
) -> JSPromise[String] =
#| (client, id) => client.interactions.get(id).then(r => JSON.stringify(r))
///|
/// Calls `client.interactions.cancel(id)` and serialises the result.
extern "js" fn ffi_interactions_cancel(
client : JSGoogleGenAI,
id : String,
) -> JSPromise[String] =
#| (client, id) => client.interactions.cancel(id).then(r => JSON.stringify(r))
///|
/// Calls `client.interactions.delete(id)`.
extern "js" fn ffi_interactions_delete(
client : JSGoogleGenAI,
id : String,
) -> JSPromise[Unit] =
#| (client, id) => client.interactions.delete(id)
// ── JSON serialisation helpers (CreateParams → String) ───────────────────────
// ─── encode helpers ───────────────────────────────────────────────────────
///|
fn opt_str(s : String?) -> Json {
match s {
Some(v) => Json::string(v)
None => null
}
}
///|
fn opt_int(n : Int?) -> Json {
match n {
Some(v) => Json::number(v.to_double())
None => null
}
}
///|
fn opt_double(d : Double?) -> Json {
match d {
Some(v) => Json::number(v)
None => null
}
}
///|
fn opt_bool(b : Bool?) -> Json {
match b {
Some(v) => Json::boolean(v)
None => null
}
}
///|
fn encode_annotation(a : Annotation) -> Json {
{
"source": opt_str(a.source),
"start_index": opt_int(a.start_index),
"end_index": opt_int(a.end_index),
}
}
///|
fn encode_content(c : Content) -> Json {
match c {
Text(t) =>
{
"type": Json::string("text"),
"text": opt_str(t.text),
"annotations": match t.annotations {
None => null
Some(arr) => Json::array(arr.map(encode_annotation))
},
}
Image(img) =>
{
"type": Json::string("image"),
"data": opt_str(img.data),
"mime_type": opt_str(img.mime_type),
"resolution": opt_str(img.resolution),
"uri": opt_str(img.uri),
}
Audio(aud) =>
{
"type": Json::string("audio"),
"data": opt_str(aud.data),
"mime_type": opt_str(aud.mime_type),
"uri": opt_str(aud.uri),
}
FunctionCall(fc) =>
{
"type": Json::string("function_call"),
"id": Json::string(fc.id),
"name": Json::string(fc.name),
"arguments": fc.arguments,
}
FunctionResult(fr) =>
{
"type": Json::string("function_result"),
"call_id": Json::string(fr.call_id),
"result": fr.result,
"name": opt_str(fr.name),
"is_error": opt_bool(fr.is_error),
}
Document(doc) =>
{
"type": Json::string("document"),
"data": opt_str(doc.data),
"mime_type": opt_str(doc.mime_type),
"uri": opt_str(doc.uri),
}
Video(vid) =>
{
"type": Json::string("video"),
"data": opt_str(vid.data),
"mime_type": opt_str(vid.mime_type),
"resolution": opt_str(vid.resolution),
"uri": opt_str(vid.uri),
}
Thought(t) =>
{
"type": Json::string("thought"),
"signature": opt_str(t.signature),
"summary": t.summary.unwrap_or(null),
}
CodeExecutionCall(c) =>
{
"type": Json::string("code_execution_call"),
"id": Json::string(c.id),
"arguments": c.arguments,
}
CodeExecutionResult(c) =>
{
"type": Json::string("code_execution_result"),
"id": Json::string(c.id),
"output": Json::string(c.output),
}
GoogleSearchCall(c) =>
{
"type": Json::string("google_search_call"),
"id": Json::string(c.id),
"arguments": c.arguments,
}
GoogleSearchResult(c) =>
{
"type": Json::string("google_search_result"),
"id": Json::string(c.id),
"result": c.result,
}
URLContextCall(c) =>
{
"type": Json::string("url_context_call"),
"id": Json::string(c.id),
"arguments": c.arguments,
}
URLContextResult(c) =>
{
"type": Json::string("url_context_result"),
"id": Json::string(c.id),
"result": c.result,
}
MCPServerToolCall(c) =>
{
"type": Json::string("mcp_server_tool_call"),
"id": Json::string(c.id),
"name": Json::string(c.name),
"arguments": c.arguments,
}
MCPServerToolResult(c) =>
{
"type": Json::string("mcp_server_tool_result"),
"id": Json::string(c.id),
"result": c.result,
}
FileSearchCall(c) =>
{
"type": Json::string("file_search_call"),
"id": Json::string(c.id),
"arguments": c.arguments,
}
FileSearchResult(c) =>
{
"type": Json::string("file_search_result"),
"id": Json::string(c.id),
"result": c.result,
}
}
}
///|
fn encode_turn_content(tc : TurnContent) -> Json {
match tc {
Plain(s) => Json::string(s)
Parts(arr) => Json::array(arr.map(encode_content))
}
}
///|
fn encode_turn(t : Turn) -> Json {
{
"content": match t.content {
Some(tc) => encode_turn_content(tc)
None => null
},
"role": opt_str(t.role),
}
}
///|
fn encode_input(input : Input) -> Json {
match input {
Plain(s) => Json::string(s)
Parts(arr) => Json::array(arr.map(encode_content))
Turns(arr) => Json::array(arr.map(encode_turn))
}
}
///|
fn encode_function_tool(f : FunctionTool) -> Json {
{
"type": Json::string("function"),
"name": Json::string(f.name),
"description": opt_str(f.description),
"parameters": f.parameters.unwrap_or(null),
}
}
///|
fn encode_tool(t : Tool) -> Json {
match t {
Function(f) => encode_function_tool(f)
GoogleSearch => { "type": Json::string("google_search") }
CodeExecution => { "type": Json::string("code_execution") }
URLContext => { "type": Json::string("url_context") }
ComputerUse(cu) =>
{
"type": Json::string("computer_use"),
"environment": opt_str(cu.environment),
"excludedPredefinedFunctions": match cu.excluded_predefined_functions {
None => null
Some(arr) => Json::array(arr.map(Json::string))
},
}
MCPServer(mcp) =>
{
"type": Json::string("mcp_server"),
"url": opt_str(mcp.url),
"name": opt_str(mcp.name),
"headers": mcp.headers.unwrap_or(null),
"allowed_tools": mcp.allowed_tools.unwrap_or(null),
}
FileSearch(fs) =>
{
"type": Json::string("file_search"),
"file_search_store_names": match fs.file_search_store_names {
None => null
Some(arr) => Json::array(arr.map(Json::string))
},
"top_k": opt_int(fs.top_k),
"metadata_filter": opt_str(fs.metadata_filter),
}
}
}
///|
fn encode_speech_config(sc : SpeechConfig) -> Json {
{
"voice": opt_str(sc.voice),
"language": opt_str(sc.language),
"speaker": opt_str(sc.speaker),
}
}
///|
fn encode_generation_config(gc : GenerationConfig) -> Json {
{
"max_output_tokens": opt_int(gc.max_output_tokens),
"temperature": opt_double(gc.temperature),
"top_p": opt_double(gc.top_p),
"seed": opt_int(gc.seed),
"stop_sequences": match gc.stop_sequences {
None => null
Some(arr) => Json::array(arr.map(Json::string))
},
"thinking_level": opt_str(gc.thinking_level),
"thinking_summaries": opt_str(gc.thinking_summaries),
"speech_config": match gc.speech_config {
None => null
Some(arr) => Json::array(arr.map(encode_speech_config))
},
"image_config": match gc.image_config {
None => null
Some(ic) =>
{
"aspect_ratio": opt_str(ic.aspect_ratio),
"image_size": opt_str(ic.image_size),
}
},
"tool_choice": gc.tool_choice.unwrap_or(null),
}
}
///|
fn encode_create_params(p : CreateParams) -> String {
let obj : Json = {
"model": Json::string(p.model),
"input": encode_input(p.input),
"previous_interaction_id": opt_str(p.previous_interaction_id),
"generation_config": match p.generation_config {
Some(gc) => encode_generation_config(gc)
None => null
},
"tools": match p.tools {
None => null
Some(ts) => Json::array(ts.map(encode_tool))
},
"response_modalities": match p.response_modalities {
None => null
Some(ms) => Json::array(ms.map(Json::string))
},
"response_format": p.response_format.unwrap_or(null),
"response_mime_type": opt_str(p.response_mime_type),
"system_instruction": opt_str(p.system_instruction),
"store": opt_bool(p.store),
"background": opt_bool(p.background),
"stream": opt_bool(p.stream),
}
Json::stringify(obj)
}
// ── JSON deserialisation helpers (String → Interaction) ──────────────────────
///|
fn decode_status(s : String) -> InteractionStatus {
match s {
"in_progress" => InProgress
"requires_action" => RequiresAction
"completed" => Completed
"failed" => Failed
"cancelled" => Cancelled
_ => Incomplete
}
}
///|
fn decode_annotation(j : Json) -> Annotation {
match j {
Object(m) =>
{
source: match m.get("source") {
Some(String(s)) => Some(s)
_ => None
},
start_index: match m.get("start_index") {
Some(Number(n, ..)) => Some(n.to_int())
_ => None
},
end_index: match m.get("end_index") {
Some(Number(n, ..)) => Some(n.to_int())
_ => None
},
}
_ => { source: None, start_index: None, end_index: None }
}
}
///|
fn decode_content(j : Json) -> Content? {
match j {
Object(m) =>
match m.get("type") {
Some(String("text")) => {
let text : String? = match m.get("text") {
Some(String(s)) => Some(s)
_ => None
}
let annotations : Array[Annotation]? = match m.get("annotations") {
Some(Array(arr)) => Some(arr.map(decode_annotation))
_ => None
}
Some(Text({ text, annotations }))
}
Some(String("image")) =>
Some(
Image({
data: match m.get("data") {
Some(String(s)) => Some(s)
_ => None
},
mime_type: match m.get("mime_type") {
Some(String(s)) => Some(s)
_ => None
},
resolution: match m.get("resolution") {
Some(String(s)) => Some(s)
_ => None
},
uri: match m.get("uri") {
Some(String(s)) => Some(s)
_ => None
},
}),
)
Some(String("audio")) =>
Some(
Audio({
data: match m.get("data") {
Some(String(s)) => Some(s)
_ => None
},
mime_type: match m.get("mime_type") {
Some(String(s)) => Some(s)
_ => None
},
uri: match m.get("uri") {
Some(String(s)) => Some(s)
_ => None
},
}),
)
Some(String("function_call")) =>
match (m.get("id"), m.get("name"), m.get("arguments")) {
(Some(String(id)), Some(String(name)), Some(args)) =>
Some(FunctionCall({ id, name, arguments: args }))
_ => None
}
Some(String("function_result")) =>
match (m.get("call_id"), m.get("result")) {
(Some(String(call_id)), Some(result)) => {
let name : String? = match m.get("name") {
Some(String(s)) => Some(s)
_ => None
}
let is_error : Bool? = match m.get("is_error") {
Some(True) => Some(true)
Some(False) => Some(false)
_ => None
}
Some(FunctionResult({ call_id, result, name, is_error }))
}
_ => None
}
Some(String("document")) =>
Some(
Document({
data: match m.get("data") {
Some(String(s)) => Some(s)
_ => None
},
mime_type: match m.get("mime_type") {
Some(String(s)) => Some(s)
_ => None
},
uri: match m.get("uri") {
Some(String(s)) => Some(s)
_ => None
},
}),
)
Some(String("video")) =>
Some(
Video({
data: match m.get("data") {
Some(String(s)) => Some(s)
_ => None
},
mime_type: match m.get("mime_type") {
Some(String(s)) => Some(s)
_ => None
},
resolution: match m.get("resolution") {
Some(String(s)) => Some(s)
_ => None
},
uri: match m.get("uri") {
Some(String(s)) => Some(s)
_ => None
},
}),
)
Some(String("thought")) =>
Some(
Thought({
signature: match m.get("signature") {
Some(String(s)) => Some(s)
_ => None
},
summary: match m.get("summary") {
Some(v) => Some(v)
_ => None
},
}),
)
Some(String("code_execution_call")) =>
match m.get("id") {
Some(String(id)) =>
Some(
CodeExecutionCall({
id,
arguments: m.get("arguments").unwrap_or(null),
}),
)
_ => None
}
Some(String("code_execution_result")) =>
match (m.get("id"), m.get("output")) {
(Some(String(id)), Some(String(output))) =>
Some(CodeExecutionResult({ id, output }))
_ => None
}
Some(String("google_search_call")) =>
match m.get("id") {
Some(String(id)) =>
Some(
GoogleSearchCall({
id,
arguments: m.get("arguments").unwrap_or(null),
}),
)
_ => None
}
Some(String("google_search_result")) =>
match m.get("id") {
Some(String(id)) =>
Some(
GoogleSearchResult({
id,
result: m.get("result").unwrap_or(null),
}),
)
_ => None
}
Some(String("url_context_call")) =>
match m.get("id") {
Some(String(id)) =>
Some(
URLContextCall({
id,
arguments: m.get("arguments").unwrap_or(null),
}),
)
_ => None
}
Some(String("url_context_result")) =>
match m.get("id") {
Some(String(id)) =>
Some(
URLContextResult({ id, result: m.get("result").unwrap_or(null) }),
)
_ => None
}
Some(String("mcp_server_tool_call")) =>
match (m.get("id"), m.get("name")) {
(Some(String(id)), Some(String(name))) =>
Some(
MCPServerToolCall({
id,
name,
arguments: m.get("arguments").unwrap_or(null),
}),
)
_ => None
}
Some(String("mcp_server_tool_result")) =>
match m.get("id") {
Some(String(id)) =>
Some(
MCPServerToolResult({
id,
result: m.get("result").unwrap_or(null),
}),
)
_ => None
}
Some(String("file_search_call")) =>
match m.get("id") {
Some(String(id)) =>
Some(
FileSearchCall({
id,
arguments: m.get("arguments").unwrap_or(null),
}),
)
_ => None
}
Some(String("file_search_result")) =>
match m.get("id") {
Some(String(id)) =>
Some(
FileSearchResult({ id, result: m.get("result").unwrap_or(null) }),
)
_ => None
}
_ => None
}
_ => None
}
}
///|
fn decode_usage(j : Json) -> Usage? {
match j {
Object(m) =>
Some({
total_input_tokens: match m.get("total_input_tokens") {
Some(Number(n, ..)) => Some(n.to_int())
_ => None
},
total_output_tokens: match m.get("total_output_tokens") {
Some(Number(n, ..)) => Some(n.to_int())
_ => None
},
total_cached_tokens: match m.get("total_cached_tokens") {
Some(Number(n, ..)) => Some(n.to_int())
_ => None
},
})
_ => None
}
}
///|
/// Parse a JSON string (as returned by the raw FFI) into an [`Interaction`].
/// Returns an error string if parsing fails.
pub fn parse_interaction(json_str : String) -> Interaction raise GenaiError {
let j : Json = @json.parse(json_str) catch {
e => raise ParseFailed(e.to_string())
}
match j {
Object(m) => {
let id = match m.get("id") {
Some(String(s)) => s
_ => raise MissingField("id")
}
let status = match m.get("status") {
Some(String(s)) => decode_status(s)
_ => raise MissingField("status")
}
let model : String? = match m.get("model") {
Some(String(s)) => Some(s)
_ => None
}
let outputs : Array[Content]? = match m.get("outputs") {
Some(Array(arr)) => {
let contents = arr.filter_map(decode_content)
if contents.length() > 0 {
Some(contents)
} else {
None
}
}
_ => None
}
let previous_interaction_id : String? = match
m.get("previous_interaction_id") {
Some(String(s)) => Some(s)
_ => None
}
let role : String? = match m.get("role") {
Some(String(s)) => Some(s)
_ => None
}
let usage : Usage? = match m.get("usage") {
Some(u) => decode_usage(u)
_ => None
}
let created : String? = match m.get("created") {
Some(String(s)) => Some(s)
_ => None
}
let updated : String? = match m.get("updated") {
Some(String(s)) => Some(s)
_ => None
}
let agent : String? = match m.get("agent") {
Some(String(s)) => Some(s)
_ => None
}
{
id,
status,
model,
outputs,
previous_interaction_id,
role,
usage,
created,
updated,
agent,
}
}
_ => raise InvalidFormat("expected JSON object")
}
}
// ── Promise utilities ─────────────────────────────────────────────────────────
///|
/// Chain a `JSPromise[String]` — calls `f` with the resolved string and
/// returns a new `JSPromise[Unit]`. Mirrors `.then()` in JavaScript.
pub extern "js" fn promise_and_then(
p : JSPromise[String],
f : (String) -> JSPromise[Unit],
) -> JSPromise[Unit] =
#| (p, f) => p.then(v => f(v))
///|
/// Sequence two `JSPromise[Unit]` values — run `f` after `p` resolves.
pub extern "js" fn promise_seq(
p : JSPromise[Unit],
f : () -> JSPromise[Unit],
) -> JSPromise[Unit] =
#| (p, f) => p.then(() => f())
///|
/// A `JSPromise` that is already resolved to `Unit`.
pub extern "js" fn promise_unit() -> JSPromise[Unit] =
#| () => Promise.resolve(undefined)
///|
/// Run a `JSPromise[Unit]` as the top-level async operation.
/// Logs errors to stderr and exits with code 1 on rejection.
pub extern "js" fn run_async(p : JSPromise[Unit]) -> Unit =
#| (p) => p.catch(e => { console.error('Error:', String(e)); process.exit(1) })
///|
/// Read a Node.js environment variable. Returns `""` if not set.
pub extern "js" fn get_env(key : String) -> String =
#| (k) => process.env[k] ?? ''
// ── Public API ───────────────────────────────────────────────────────────────
///|
/// Create a new `GoogleGenAI` client.
///
/// ```moonbit nocheck
/// let ai = GoogleGenAI::new("YOUR_GEMINI_API_KEY")
/// ```
pub fn GoogleGenAI::new(api_key : String) -> GoogleGenAI {
{ js: ffi_new_client(api_key) }
}
///|
/// Create a `GoogleGenAI` client that routes all requests through a custom
/// `base_url` (e.g. an internal reverse-proxy).
///
/// ```moonbit nocheck
/// let ai = GoogleGenAI::new_with_base_url(
/// "YOUR_GEMINI_API_KEY", "https://ja3.chenyong.life",
/// )
/// ```
pub fn GoogleGenAI::new_with_base_url(
api_key : String,
base_url : String,
) -> GoogleGenAI {
{ js: ffi_new_client_with_base_url(api_key, base_url) }
}
///|
/// Create a new interaction and return a `JSPromise` that resolves to a
/// JSON-encoded [`Interaction`] string.
///
/// Use [`parse_interaction`] to convert the resolved string to a typed
/// [`Interaction`] value.
///
/// ## Example (stateless, single turn)
/// ```moonbit nocheck
/// let p = ai.interactions_create({
/// model: "gemini-2.5-flash",
/// input: Input::Plain("Why is the sky blue?"),
/// previous_interaction_id: None,
/// generation_config: None,
/// tools: None,
/// response_modalities: None,
/// response_format: None,
/// response_mime_type: None,
/// system_instruction: None,
/// store: None,
/// })
/// ```
///
/// ## Example (stateful multi-turn)
/// ```moonbit nocheck
/// let p2 = ai.interactions_create({
/// ...,
/// input: Input::Plain("What about sunsets?"),
/// previous_interaction_id: Some(first_interaction_id),
/// })
/// ```
pub fn GoogleGenAI::interactions_create(
self : GoogleGenAI,
params : CreateParams,
) -> JSPromise[String] {
ffi_interactions_create(self.js, encode_create_params(params))
}
///|
/// Retrieve a previously created interaction by its `id`.
///
/// Returns a `JSPromise` that resolves to a JSON-encoded [`Interaction`].
pub fn GoogleGenAI::interactions_get(
self : GoogleGenAI,
id : String,
) -> JSPromise[String] {
ffi_interactions_get(self.js, id)
}
///|
/// Cancel a background interaction that is still running.
///
/// Only applies to interactions started with `background: true` in
/// [`GenerationConfig`]. Returns a `JSPromise` resolving to the updated
/// [`Interaction`].
pub fn GoogleGenAI::interactions_cancel(
self : GoogleGenAI,
id : String,
) -> JSPromise[String] {
ffi_interactions_cancel(self.js, id)
}
///|
/// Delete an interaction by its `id`.
pub fn GoogleGenAI::interactions_delete(
self : GoogleGenAI,
id : String,
) -> JSPromise[Unit] {
ffi_interactions_delete(self.js, id)
}
// ── Streaming support ────────────────────────────────────────────────────────
///|
/// SSE event types emitted during a streamed interaction.
/// **Untested** — not yet verified at runtime.
pub(all) enum InteractionEvent {
/// The interaction has started; carries the initial snapshot as JSON.
InteractionStart(Json)
/// The interaction status has changed.
StatusUpdate(String)
/// A new content block has started at the given index.
ContentStart(Int, String)
/// A delta for the content block at the given index.
ContentDelta(Int, Json)
/// The content block at the given index has finished.
ContentStop(Int)
/// The interaction is complete; carries the final snapshot as JSON.
InteractionComplete(Json)
/// An error event.
Error(Json)
/// An unrecognised event type.
Unknown(String, Json)
} derive(Show)
///|
/// Parse a JSON-encoded SSE event string into an [`InteractionEvent`].
/// **Untested** — not yet verified at runtime.
pub fn parse_event(json_str : String) -> InteractionEvent raise GenaiError {
let j : Json = @json.parse(json_str) catch {
e => raise ParseFailed(e.to_string())
}
match j {
Object(m) =>
match m.get("event_type") {
Some(String("interaction.start")) => InteractionStart(j)
Some(String("interaction.status_update")) =>
match m.get("status") {
Some(String(s)) => StatusUpdate(s)
_ => StatusUpdate("")
}
Some(String("content.start")) => {
let index = match m.get("index") {
Some(Number(n, ..)) => n.to_int()
_ => 0
}
let content_type = match m.get("content") {
Some(Object(cm)) =>
match cm.get("type") {
Some(String(s)) => s
_ => ""
}
_ => ""
}
ContentStart(index, content_type)
}
Some(String("content.delta")) => {
let index = match m.get("index") {
Some(Number(n, ..)) => n.to_int()
_ => 0
}
let delta = m.get("delta").unwrap_or(null)
ContentDelta(index, delta)
}
Some(String("content.stop")) => {
let index = match m.get("index") {
Some(Number(n, ..)) => n.to_int()
_ => 0
}
ContentStop(index)
}
Some(String("interaction.complete")) => InteractionComplete(j)
Some(String("error")) => Error(j)
Some(String(t)) => Unknown(t, j)
_ => raise InvalidFormat("event missing 'event_type' field")
}
_ => raise InvalidFormat("expected JSON object for event")
}
}
///|
/// Low-level FFI for streaming interaction creation.
/// Iterates the async stream and calls `on_event` for each SSE event.
extern "js" fn ffi_interactions_create_stream(
client : JSGoogleGenAI,
params_json : String,
on_event : (String) -> Unit,
) -> JSPromise[Unit] =
#| (client, p, cb) => {
#| const strip = v => {
#| if (Array.isArray(v)) return v.map(strip)
#| if (v !== null && typeof v === 'object')
#| return Object.fromEntries(
#| Object.entries(v).filter(([,x]) => x !== null).map(([k,x]) => [k, strip(x)])
#| )
#| return v
#| }
#| const params = strip(JSON.parse(p))
#| params.stream = true
#| return client.interactions.create(params).then(async stream => {
#| for await (const event of stream) { cb(JSON.stringify(event)) }
#| })
#| }
///|
/// Create an interaction with streaming. Each SSE event is delivered to
/// `on_event` as a JSON string; use [`parse_event`] to decode it.
///
/// **Untested** — not yet verified at runtime.
///
/// ## Example
/// ```moonbit nocheck
/// let p = ai.interactions_create_stream(
/// {
/// model: "gemini-2.5-flash",
/// input: Input::Plain("Hello!"),
/// ..CreateParams::default()
/// },
/// fn(event_json) {
/// // handle each SSE event
/// },
/// )
/// ```
pub fn GoogleGenAI::interactions_create_stream(
self : GoogleGenAI,
params : CreateParams,
on_event : (String) -> Unit,
) -> JSPromise[Unit] {
ffi_interactions_create_stream(
self.js,
encode_create_params(params),
on_event,
)
}