melchizedek-agents 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +12 -0
- package/CHANGELOG.md +825 -0
- package/DOCUMENTATION.md +217 -59
- package/README.md +42 -1
- package/config/agents/examples/harness.yaml +3 -1
- package/config/agents/examples/model_zoo.yaml +10 -9
- package/config/agents/syndicateSchema.yaml +50 -16
- package/config/agents/templates/account_memory.yaml +2 -6
- package/config/agents/templates/case_desk.yaml +3 -9
- package/config/agents/templates/draft_review.yaml +3 -9
- package/config/agents/templates/intake_extractor.yaml +1 -3
- package/config/agents/templates/research_brief.yaml +3 -9
- package/config/agents/templates/research_desk.yaml +3 -9
- package/config/agents/templates/review_panel.yaml +4 -12
- package/config/agents/templates/support_triage.yaml +4 -12
- package/config/agents/templates/systems_operator.yaml +2 -6
- package/db/hardening.sql +7 -1
- package/db/migrations/0013_tool_credentials.sql +260 -0
- package/dist/lib/a2a/app.d.ts +70 -6
- package/dist/lib/a2a/app.js +107 -2
- package/dist/lib/a2a/executor.d.ts +20 -3
- package/dist/lib/a2a/executor.js +66 -7
- package/dist/lib/a2a/remoteAgent.d.ts +24 -9
- package/dist/lib/a2a/remoteAgent.js +31 -14
- package/dist/lib/compile.d.ts +121 -30
- package/dist/lib/compile.js +137 -124
- package/dist/lib/compileAdk.d.ts +29 -0
- package/dist/lib/compileAdk.js +132 -0
- package/dist/lib/compileNative.d.ts +85 -0
- package/dist/lib/compileNative.js +222 -0
- package/dist/lib/config.d.ts +6 -5
- package/dist/lib/config.js +6 -5
- package/dist/lib/doctor.js +1 -0
- package/dist/lib/index.d.ts +2 -1
- package/dist/lib/index.js +2 -1
- package/dist/lib/loadSyndicate.d.ts +18 -0
- package/dist/lib/memory/erase.d.ts +5 -3
- package/dist/lib/memory/erase.js +5 -2
- package/dist/lib/memory/namespace.d.ts +8 -1
- package/dist/lib/memory/namespace.js +12 -6
- package/dist/lib/memory/providers.js +2 -1
- package/dist/lib/memory/supabaseMemoryService.d.ts +37 -3
- package/dist/lib/memory/supabaseMemoryService.js +42 -2
- package/dist/lib/model.d.ts +47 -0
- package/dist/lib/model.js +37 -0
- package/dist/lib/models/adapterResolver.d.ts +113 -0
- package/dist/lib/models/adapterResolver.js +136 -0
- package/dist/lib/models/adkGeminiAdapter.d.ts +90 -0
- package/dist/lib/models/adkGeminiAdapter.js +367 -0
- package/dist/lib/models/adkShim.d.ts +111 -0
- package/dist/lib/models/adkShim.js +136 -0
- package/dist/lib/models/capabilities.d.ts +3 -0
- package/dist/lib/models/capabilities.js +20 -13
- package/dist/lib/models/chatCompletionsAdapter.d.ts +279 -0
- package/dist/lib/models/chatCompletionsAdapter.js +854 -0
- package/dist/lib/models/circuitBreaker.d.ts +42 -0
- package/dist/lib/models/circuitBreaker.js +70 -0
- package/dist/lib/models/claudeAdapter.d.ts +103 -0
- package/dist/lib/models/claudeAdapter.js +669 -0
- package/dist/lib/models/claudeLlm.d.ts +39 -62
- package/dist/lib/models/claudeLlm.js +44 -349
- package/dist/lib/models/claudeModels.d.ts +127 -0
- package/dist/lib/models/claudeModels.js +237 -0
- package/dist/lib/models/contract.d.ts +395 -0
- package/dist/lib/models/contract.js +39 -0
- package/dist/lib/models/endpoints.js +2 -1
- package/dist/lib/models/errorResponse.d.ts +50 -0
- package/dist/lib/models/errorResponse.js +48 -0
- package/dist/lib/models/fallback.d.ts +17 -10
- package/dist/lib/models/fallback.js +53 -42
- package/dist/lib/models/fallbackAdapter.d.ts +55 -0
- package/dist/lib/models/fallbackAdapter.js +62 -0
- package/dist/lib/models/gateway.js +2 -1
- package/dist/lib/models/gatewayAdapter.d.ts +51 -0
- package/dist/lib/models/gatewayAdapter.js +81 -0
- package/dist/lib/models/gatewayLlm.d.ts +12 -33
- package/dist/lib/models/gatewayLlm.js +14 -75
- package/dist/lib/models/geminiAdapter.d.ts +123 -0
- package/dist/lib/models/geminiAdapter.js +831 -0
- package/dist/lib/models/geminiState.d.ts +25 -0
- package/dist/lib/models/geminiState.js +25 -0
- package/dist/lib/models/genaiMapping.d.ts +263 -0
- package/dist/lib/models/genaiMapping.js +862 -0
- package/dist/lib/models/gptAdapter.d.ts +240 -0
- package/dist/lib/models/gptAdapter.js +804 -0
- package/dist/lib/models/gptLlm.d.ts +63 -123
- package/dist/lib/models/gptLlm.js +88 -602
- package/dist/lib/models/grokAdapter.d.ts +86 -0
- package/dist/lib/models/grokAdapter.js +142 -0
- package/dist/lib/models/grokLlm.d.ts +22 -45
- package/dist/lib/models/grokLlm.js +22 -67
- package/dist/lib/models/kimiAdapter.d.ts +157 -0
- package/dist/lib/models/kimiAdapter.js +203 -0
- package/dist/lib/models/kimiLlm.d.ts +15 -107
- package/dist/lib/models/kimiLlm.js +17 -153
- package/dist/lib/models/ollamaAdapter.d.ts +125 -0
- package/dist/lib/models/ollamaAdapter.js +155 -0
- package/dist/lib/models/ollamaLlm.d.ts +11 -107
- package/dist/lib/models/ollamaLlm.js +13 -151
- package/dist/lib/models/openAiCompatibleLlm.d.ts +73 -216
- package/dist/lib/models/openAiCompatibleLlm.js +109 -671
- package/dist/lib/models/providerState.d.ts +63 -0
- package/dist/lib/models/providerState.js +61 -0
- package/dist/lib/models/reasoning.d.ts +26 -0
- package/dist/lib/models/reasoning.js +59 -0
- package/dist/lib/models/registry.d.ts +58 -35
- package/dist/lib/models/registry.js +89 -118
- package/dist/lib/models/retry.d.ts +2 -1
- package/dist/lib/models/retry.js +3 -2
- package/dist/lib/models/schemaNormalize.d.ts +98 -0
- package/dist/lib/models/schemaNormalize.js +321 -1
- package/dist/lib/models/tracedGemini.d.ts +41 -0
- package/dist/lib/models/tracedGemini.js +98 -0
- package/dist/lib/models/urls.d.ts +11 -0
- package/dist/lib/models/urls.js +16 -0
- package/dist/lib/net/redirects.d.ts +21 -2
- package/dist/lib/net/redirects.js +11 -34
- package/dist/lib/observability/audit.d.ts +5 -1
- package/dist/lib/observability/lineage.d.ts +14 -0
- package/dist/lib/observability/lineage.js +33 -0
- package/dist/lib/observability/supabaseSpanExporter.d.ts +10 -4
- package/dist/lib/observability/supabaseSpanExporter.js +17 -9
- package/dist/lib/observability/tracer.d.ts +26 -8
- package/dist/lib/observability/tracer.js +43 -18
- package/dist/lib/runtime/adkMemoryBridge.d.ts +46 -0
- package/dist/lib/runtime/adkMemoryBridge.js +68 -0
- package/dist/lib/runtime/adkSessionBridge.d.ts +78 -0
- package/dist/lib/runtime/adkSessionBridge.js +176 -0
- package/dist/lib/runtime/credentials.d.ts +59 -0
- package/dist/lib/runtime/credentials.js +62 -0
- package/dist/lib/runtime/events.d.ts +229 -0
- package/dist/lib/runtime/events.js +274 -0
- package/dist/lib/runtime/logging.d.ts +32 -0
- package/dist/lib/runtime/logging.js +55 -0
- package/dist/lib/runtime/memoryService.d.ts +72 -0
- package/dist/lib/runtime/memoryService.js +25 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +203 -0
- package/dist/lib/runtime/native/agentLoop.js +687 -0
- package/dist/lib/runtime/native/compaction.d.ts +103 -0
- package/dist/lib/runtime/native/compaction.js +191 -0
- package/dist/lib/runtime/native/delegate.d.ts +98 -0
- package/dist/lib/runtime/native/delegate.js +173 -0
- package/dist/lib/runtime/native/history.d.ts +83 -0
- package/dist/lib/runtime/native/history.js +312 -0
- package/dist/lib/runtime/native/interrupts.d.ts +126 -0
- package/dist/lib/runtime/native/interrupts.js +291 -0
- package/dist/lib/runtime/native/request.d.ts +215 -0
- package/dist/lib/runtime/native/request.js +585 -0
- package/dist/lib/runtime/native/selfCorrection.d.ts +106 -0
- package/dist/lib/runtime/native/selfCorrection.js +253 -0
- package/dist/lib/runtime/native/step.d.ts +148 -0
- package/dist/lib/runtime/native/step.js +239 -0
- package/dist/lib/runtime/native/taskMode.d.ts +64 -0
- package/dist/lib/runtime/native/taskMode.js +126 -0
- package/dist/lib/runtime/native/telemetry.d.ts +75 -0
- package/dist/lib/runtime/native/telemetry.js +299 -0
- package/dist/lib/runtime/native/tempState.d.ts +31 -0
- package/dist/lib/runtime/native/tempState.js +56 -0
- package/dist/lib/runtime/nativeTurn.d.ts +86 -0
- package/dist/lib/runtime/nativeTurn.js +107 -0
- package/dist/lib/runtime/questions.d.ts +29 -7
- package/dist/lib/runtime/questions.js +47 -32
- package/dist/lib/runtime/runtimeFlag.d.ts +30 -0
- package/dist/lib/runtime/runtimeFlag.js +50 -0
- package/dist/lib/runtime/sessions.d.ts +166 -0
- package/dist/lib/runtime/sessions.js +201 -0
- package/dist/lib/runtime/syndicateTurn.d.ts +41 -5
- package/dist/lib/runtime/syndicateTurn.js +202 -32
- package/dist/lib/session/supabaseSessionService.d.ts +64 -17
- package/dist/lib/session/supabaseSessionService.js +148 -105
- package/dist/lib/session/transcript.d.ts +68 -22
- package/dist/lib/session/transcript.js +102 -28
- package/dist/lib/skills.js +33 -7
- package/dist/lib/storage/postgres/credentialStore.d.ts +20 -0
- package/dist/lib/storage/postgres/credentialStore.js +82 -0
- package/dist/lib/storage/postgres/index.d.ts +12 -0
- package/dist/lib/storage/postgres/index.js +6 -1
- package/dist/lib/storage/postgres/sessionService.d.ts +43 -15
- package/dist/lib/storage/postgres/sessionService.js +128 -66
- package/dist/lib/syndicateSchema.d.ts +47 -0
- package/dist/lib/syndicateSchema.js +72 -3
- package/dist/lib/toolRegistry.d.ts +15 -2
- package/dist/lib/toolRegistry.js +33 -17
- package/dist/lib/tools/adkTool.d.ts +81 -0
- package/dist/lib/tools/adkTool.js +268 -0
- package/dist/lib/tools/auth.d.ts +118 -0
- package/dist/lib/tools/auth.js +89 -0
- package/dist/lib/tools/collectionsSearchTool.d.ts +7 -6
- package/dist/lib/tools/collectionsSearchTool.js +11 -26
- package/dist/lib/tools/credentialCipher.d.ts +58 -0
- package/dist/lib/tools/credentialCipher.js +116 -0
- package/dist/lib/tools/credentialStore.d.ts +77 -0
- package/dist/lib/tools/credentialStore.js +246 -0
- package/dist/lib/tools/examples.d.ts +43 -0
- package/dist/lib/tools/examples.js +62 -0
- package/dist/lib/tools/generateImageTool.d.ts +15 -6
- package/dist/lib/tools/generateImageTool.js +19 -29
- package/dist/lib/tools/inspectImageTool.d.ts +9 -6
- package/dist/lib/tools/inspectImageTool.js +13 -17
- package/dist/lib/tools/mcpToolFactory.d.ts +42 -1
- package/dist/lib/tools/mcpToolFactory.js +46 -47
- package/dist/lib/tools/memoryTools.d.ts +61 -0
- package/dist/lib/tools/memoryTools.js +119 -0
- package/dist/lib/tools/nativeTools.d.ts +38 -0
- package/dist/lib/tools/nativeTools.js +43 -0
- package/dist/lib/tools/oauthConsent.d.ts +181 -0
- package/dist/lib/tools/oauthConsent.js +330 -0
- package/dist/lib/tools/openapi/call.d.ts +108 -0
- package/dist/lib/tools/openapi/call.js +295 -0
- package/dist/lib/tools/openapi/parse.d.ts +147 -0
- package/dist/lib/tools/openapi/parse.js +570 -0
- package/dist/lib/tools/openapiTools.d.ts +25 -17
- package/dist/lib/tools/openapiTools.js +124 -121
- package/dist/lib/tools/scienceTools.d.ts +14 -14
- package/dist/lib/tools/skillToolset.d.ts +58 -87
- package/dist/lib/tools/skillToolset.js +61 -142
- package/dist/lib/tools/skills/env.d.ts +58 -0
- package/dist/lib/tools/skills/env.js +165 -0
- package/dist/lib/tools/skills/executor.d.ts +114 -0
- package/dist/lib/tools/skills/executor.js +331 -0
- package/dist/lib/tools/skills/frontmatter.d.ts +79 -0
- package/dist/lib/tools/skills/frontmatter.js +215 -0
- package/dist/lib/tools/skills/loader.d.ts +91 -0
- package/dist/lib/tools/skills/loader.js +257 -0
- package/dist/lib/tools/skills/tools.d.ts +187 -0
- package/dist/lib/tools/skills/tools.js +386 -0
- package/dist/lib/tools/taskTools.d.ts +15 -15
- package/dist/lib/tools/tool.d.ts +354 -0
- package/dist/lib/tools/tool.js +304 -0
- package/dist/lib/tools/toolContract.d.ts +89 -33
- package/dist/lib/tools/toolContract.js +104 -82
- package/dist/lib/tools/urlContextTool.d.ts +3 -0
- package/dist/lib/tools/urlContextTool.js +5 -1
- package/dist/lib/tools/webExtractTool.d.ts +2 -2
- package/dist/lib/tools/webExtractTool.js +2 -1
- package/dist/lib/tools/webSearchTool.d.ts +6 -9
- package/dist/lib/tools/webSearchTool.js +14 -59
- package/dist/lib/tools/wikiTools.d.ts +19 -19
- package/dist/lib/tools/wikiTools.js +13 -9
- package/dist/lib/tools/xApiSearchTool.d.ts +2 -2
- package/dist/lib/tools/xApiSearchTool.js +2 -1
- package/dist/lib/tools/xSearchTool.d.ts +7 -9
- package/dist/lib/tools/xSearchTool.js +11 -62
- package/dist/lib/tools/xaiSearchParams.d.ts +41 -0
- package/dist/lib/tools/xaiSearchParams.js +126 -0
- package/dist/lib/wiki/agentRun.d.ts +12 -0
- package/dist/lib/wiki/agentRun.js +72 -34
- package/dist/lib/wiki/entities.d.ts +12 -0
- package/dist/lib/wiki/entities.js +45 -3
- package/dist/lib/workflow/agentNode.d.ts +136 -0
- package/dist/lib/workflow/agentNode.js +274 -0
- package/dist/lib/workflow/graph.d.ts +177 -0
- package/dist/lib/workflow/graph.js +458 -0
- package/dist/lib/workflow/nodeEvents.d.ts +50 -0
- package/dist/lib/workflow/nodeEvents.js +101 -0
- package/dist/lib/workflow/pause.d.ts +122 -0
- package/dist/lib/workflow/pause.js +180 -0
- package/dist/lib/workflow/resume.d.ts +165 -0
- package/dist/lib/workflow/resume.js +440 -0
- package/dist/lib/workflow/route.d.ts +58 -0
- package/dist/lib/workflow/route.js +69 -0
- package/dist/lib/workflow/scheduler.d.ts +415 -0
- package/dist/lib/workflow/scheduler.js +743 -0
- package/dist/lib/workflow/toolNode.d.ts +127 -0
- package/dist/lib/workflow/toolNode.js +236 -0
- package/dist/lib/workflow/turn.d.ts +118 -0
- package/dist/lib/workflow/turn.js +216 -0
- package/dist/lib/workflowConfig.d.ts +3 -2
- package/dist/lib/workflowConfig.js +3 -12
- package/dist/scripts/a2a_server.js +4 -3
- package/dist/scripts/assistant_worker.d.ts +2 -1
- package/dist/scripts/assistant_worker.js +9 -4
- package/dist/scripts/syndicate_chat.js +19 -10
- package/package.json +4 -1
- package/skills/briefs/melchizedek-author.md +3 -2
- package/skills/briefs/melchizedek-models.md +13 -9
- package/skills/briefs/melchizedek.md +3 -3
- package/skills/melchizedek/SKILL.md +2 -1
- package/skills/melchizedek-author/SKILL.md +2 -1
- package/skills/melchizedek-author/assets/minimal.yaml +2 -0
- package/skills/melchizedek-models/SKILL.md +23 -6
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/genaiMapping.ts — @google/genai `Content`, and ADK's LlmRequest
|
|
3
|
+
* and LlmResponse, to and from the engine's model contract
|
|
4
|
+
* (lib/models/contract.ts, ADR 0048).
|
|
5
|
+
*
|
|
6
|
+
* WHY this file exists:
|
|
7
|
+
* The ADK runtime holds and stores @google/genai `Content`: the history of
|
|
8
|
+
* an LlmRequest, and the `content` of every stored event. The native
|
|
9
|
+
* runtime and every adapter on the contract speak `Message` and `Part`.
|
|
10
|
+
* This module converts between the two, so the native runtime can read a
|
|
11
|
+
* session ADK stored, and an adapter can move onto the contract while ADK
|
|
12
|
+
* still runs: its ADK class maps the LlmRequest in and each ModelResponse
|
|
13
|
+
* out. It is pure (no I/O, clock or randomness) and never mutates its
|
|
14
|
+
* input; leaf values (args, results, payloads) are shared, not copied.
|
|
15
|
+
* The contract stays a leaf: only this module names @google/*.
|
|
16
|
+
* wiki/models/model-contract.md ("From genai Content") is the spec.
|
|
17
|
+
*
|
|
18
|
+
* LOSSLESS FOR STORED CONTENT. Content → contract → Content gives back the
|
|
19
|
+
* same JSON, keys aside (both event tables are jsonb, which keeps no key
|
|
20
|
+
* order), for every part kind. tests/genaiMapping.test.ts runs every stored
|
|
21
|
+
* session fixture through it.
|
|
22
|
+
*
|
|
23
|
+
* Roles. `model` → assistant, `system` → system, a `user` content made
|
|
24
|
+
* only of functionResponse parts → tool, any other `user` content → user.
|
|
25
|
+
* Back: assistant → `model`, system → `system`, user and tool → `user`.
|
|
26
|
+
* A content with no role reads as user and comes back as `user`. A user
|
|
27
|
+
* content that mixes results with other parts is a user message, its
|
|
28
|
+
* results described as text (below), because a ToolMessage holds only
|
|
29
|
+
* results; the calls they answered are then left unanswered there. The
|
|
30
|
+
* engine's surfaces send an approval or an answer as a part of its own.
|
|
31
|
+
*
|
|
32
|
+
* Parts. Each genai part becomes one contract part:
|
|
33
|
+
* { text } ↔ TextPart
|
|
34
|
+
* { text, thought: true } ↔ ThinkingPart
|
|
35
|
+
* { functionCall: {name,args,id} } ↔ ToolCallPart
|
|
36
|
+
* { functionResponse: {id,name,response} } ↔ ToolResultPart
|
|
37
|
+
* { inlineData: {mimeType,data} } ↔ BlobPart with data
|
|
38
|
+
* { fileData: {mimeType,fileUri} } ↔ BlobPart with url
|
|
39
|
+
* A part's `thoughtSignature` ↔ providerState { provider: 'gemini', kind:
|
|
40
|
+
* 'thought_signature', payload: <signature> } on the same part, and any
|
|
41
|
+
* providerState another adapter wrote passes through unchanged.
|
|
42
|
+
*
|
|
43
|
+
* Everything else is carried whole. A part the contract cannot hold
|
|
44
|
+
* exactly (Gemini code execution, a field such as `videoMetadata` or
|
|
45
|
+
* `displayName`, a call with no `args`, a signature beside another
|
|
46
|
+
* provider's state) or cannot place in its message (a functionCall in a
|
|
47
|
+
* `user` content, as ADK writes `adk_request_confirmation`) keeps the
|
|
48
|
+
* original genai part as providerState { provider: 'gemini', kind:
|
|
49
|
+
* 'genai_part', payload: <the part> }, on the nearest part the message
|
|
50
|
+
* allows: a functionCall or functionResponse as itself where it may stand,
|
|
51
|
+
* else as text describing it; `executableCode` as its code in a fenced
|
|
52
|
+
* block; `codeExecutionResult` as its output; anything unknown as empty
|
|
53
|
+
* text. Back, that state is the genai part, verbatim. So only the Gemini
|
|
54
|
+
* adapter (provider `gemini`) replays it, and every other adapter sees
|
|
55
|
+
* the projection.
|
|
56
|
+
*
|
|
57
|
+
* Tool results. `response` is the result when it is an object; a response
|
|
58
|
+
* of exactly `{ result: <not an object> }` is that value; exactly
|
|
59
|
+
* `{ error: <not null or false> }` is that value with `isError: true` (a
|
|
60
|
+
* tool ADK caught throwing). Back: `{ error: result }` for an error, the
|
|
61
|
+
* result when it is an object, else `{ result }`. A tool's successful
|
|
62
|
+
* object result shaped `{ error }` therefore reads back as an error: the
|
|
63
|
+
* genai bytes cannot tell the two apart.
|
|
64
|
+
*
|
|
65
|
+
* Ids. A ToolCallPart always has an id. A genai call without one (Gemini
|
|
66
|
+
* returns none, and ADK strips its own `adk-` ids from every request) gets
|
|
67
|
+
* `genai-noid-<content>-<part>`, from its position, and a result without
|
|
68
|
+
* one takes the id of the latest open minted call of the same name, else
|
|
69
|
+
* one of its own (it then answers no call in the history). Back, a minted
|
|
70
|
+
* id is left off again, so the round trip restores the absence. Stored
|
|
71
|
+
* events keep the ids ADK assigned, which pass through as they are.
|
|
72
|
+
*
|
|
73
|
+
* THE REQUEST (llmRequestToModelRequest). The system instruction's text;
|
|
74
|
+
* the contents as above; client tools from `toolsDict` through
|
|
75
|
+
* contractToolDeclaration (lib/models/schemaNormalize.ts), which converts
|
|
76
|
+
* Gemini's dialect once; native tools from the toolsDict entries
|
|
77
|
+
* nativeToolOf names (the web_search, x_search and collections_search
|
|
78
|
+
* sentinels) and from Gemini's `config.tools` entries (`googleSearch` and
|
|
79
|
+
* `googleSearchRetrieval` → web_search, `urlContext`, `codeExecution`);
|
|
80
|
+
* `responseJsonSchema` or `responseSchema` as a lowercase outputSchema, and
|
|
81
|
+
* `responseMimeType: 'application/json'` with neither as `outputFormat:
|
|
82
|
+
* 'json'` (JSON mode, ADR 0061); the function-calling mode as toolChoice
|
|
83
|
+
* (`VALIDATED` as strict tools); the reasoning fields as a ReasoningSetting where it is exact (reasoningOf);
|
|
84
|
+
* sampling; stream; the abort signal. Not mapped: tool options inside a
|
|
85
|
+
* `config.tools` entry, any other `config.tools` entry, `includeThoughts`,
|
|
86
|
+
* and the generateContentConfig fields the contract leaves out (topK, seed,
|
|
87
|
+
* penalties, safetySettings).
|
|
88
|
+
*
|
|
89
|
+
* THE RESPONSE (modelResponseToLlmResponse). Parts back to a `model`
|
|
90
|
+
* content; a partial as `partial: true`, the final with `turnComplete:
|
|
91
|
+
* true`; usage in Gemini's meanings (usageToMetadata); error as `errorCode`
|
|
92
|
+
* and `errorMessage`, key-shaped text scrubbed, with `retryable` and
|
|
93
|
+
* `status` as customMetadata['error.retryable'] and ['error.status']
|
|
94
|
+
* (withRetryVerdict, lib/models/errorResponse.ts), the only place
|
|
95
|
+
* FallbackLlm reads whether a fallback may answer; finishReason as
|
|
96
|
+
* Gemini's, except that an error whose code is one of Gemini's finish
|
|
97
|
+
* reasons (the Gemini adapters keep it verbatim: MALFORMED_FUNCTION_CALL,
|
|
98
|
+
* RECITATION, ...) gets that reason itself, as ADK's Gemini reports both, so
|
|
99
|
+
* self-correction retries a malformed call and the stored event reads the
|
|
100
|
+
* same on either runtime (ADR 0088); grounding as the `webSearchQueries` and
|
|
101
|
+
* `groundingChunks[].web` that lib/grounding.ts reads. Not carried, since
|
|
102
|
+
* an LlmResponse has no field for them: `cacheWriteTokens`, a citation's
|
|
103
|
+
* span and cited text, and which native tool ran a query.
|
|
104
|
+
*
|
|
105
|
+
* THE REVERSE: a contract adapter over an ADK BaseLlm (lib/models/
|
|
106
|
+
* adkGeminiAdapter.ts, the wrapper over ADK's Gemini) maps the ModelRequest
|
|
107
|
+
* it is given to an LlmRequest, and each LlmResponse back.
|
|
108
|
+
*
|
|
109
|
+
* modelRequestToLlmRequest builds the LlmRequest ADK's path builds for a
|
|
110
|
+
* Gemini model: the system prompt as `systemInstruction`; the messages as
|
|
111
|
+
* above; client tools as `functionDeclarations` with the lowercase schema
|
|
112
|
+
* in `parametersJsonSchema`, and in `toolsDict` as declaration-only
|
|
113
|
+
* entries, where llmRequestToModelRequest and the ADK-path adapters read
|
|
114
|
+
* them; native tools as Gemini's tool objects, in the request's order;
|
|
115
|
+
* toolChoice and strict as the function-calling mode, sent only beside
|
|
116
|
+
* declarations; the output schema as `responseJsonSchema`, and
|
|
117
|
+
* `outputFormat: 'json'` without one as `responseMimeType:
|
|
118
|
+
* 'application/json'`; reasoning
|
|
119
|
+
* through reasoningConfig (lib/models/reasoning.ts), as the compiler maps
|
|
120
|
+
* it; sampling; the signal as `config.abortSignal`. What does not come
|
|
121
|
+
* back the same through llmRequestToModelRequest: `google_search` reads as
|
|
122
|
+
* `web_search`; `x_search` and `collections_search` are left out (Gemini
|
|
123
|
+
* has no tool for them, and ADK adds their sentinels only for xAI); `auto`
|
|
124
|
+
* reads as absent, and a choice without tools is not sent; one strict tool
|
|
125
|
+
* makes every tool strict, and strict is lost beside a forced choice; a
|
|
126
|
+
* level a model renders as a budget or another word reads back as that
|
|
127
|
+
* rendering; `outputFormat: 'json'` beside a schema reads back as the
|
|
128
|
+
* schema alone; `stream` is not an LlmRequest field. System messages stay
|
|
129
|
+
* `system` contents, which Gemini refuses: the caller folds them into the
|
|
130
|
+
* system prompt.
|
|
131
|
+
*
|
|
132
|
+
* llmResponseToModelResponse reads one LlmResponse. A partial is its text
|
|
133
|
+
* and thinking deltas. Anything else is a final: its parts as an assistant
|
|
134
|
+
* message's, minus thinking, whose signature moves to the next part (or
|
|
135
|
+
* stays with the last part when none follows); `model`, when given, on
|
|
136
|
+
* every thought_signature state, as GeminiAdapter writes it; ids minted
|
|
137
|
+
* from `index`, the answer's place in its conversation; `errorCode` as the
|
|
138
|
+
* error, with the verdict read back from customMetadata and key-shaped text
|
|
139
|
+
* scrubbed, except ADK's `STOP`, which is no error; Gemini's finish reason
|
|
140
|
+
* as the contract's (`tool_call` whenever a call is there); usage under
|
|
141
|
+
* the contract's meanings; grounding as the cited pages and the search
|
|
142
|
+
* queries, attributed to `searchTool`. Not read back: citation spans and
|
|
143
|
+
* cited text, `cacheWriteTokens`, and any thought text in a final, which is
|
|
144
|
+
* display only (the caller yields it as a partial).
|
|
145
|
+
*
|
|
146
|
+
* USAGE MEANINGS. usageToMetadata and usageFromMetadata use Gemini's: its
|
|
147
|
+
* `candidatesTokenCount` excludes the thinking in `thoughtsTokenCount`. The
|
|
148
|
+
* ADK-path GPT and chat-completions adapters write `candidatesTokenCount`
|
|
149
|
+
* with reasoning included, so usageFromMetadata on an event they stored
|
|
150
|
+
* counts that reasoning twice in `outputTokens`.
|
|
151
|
+
*/
|
|
152
|
+
import { FinishReason, FunctionCallingConfigMode } from '@google/genai';
|
|
153
|
+
import { ERROR_RETRYABLE_KEY, ERROR_STATUS_KEY, errorText, withRetryVerdict } from './errorResponse.js';
|
|
154
|
+
import { GEMINI_PROVIDER, MINTED_CALL_ID_PREFIX, THOUGHT_SIGNATURE_KIND } from './geminiState.js';
|
|
155
|
+
import { reasoningConfig } from './reasoning.js';
|
|
156
|
+
import { contractToolDeclaration, nativeToolOf, toContractJsonSchema } from './schemaNormalize.js';
|
|
157
|
+
/**
|
|
158
|
+
* The provider id the mapping writes its own state under (only a Gemini
|
|
159
|
+
* adapter replays it), and the providerState kind for a Gemini
|
|
160
|
+
* `thoughtSignature`, whose payload is the signature. Defined once in
|
|
161
|
+
* lib/models/geminiState.ts, so the Gemini adapter spells them the same.
|
|
162
|
+
*/
|
|
163
|
+
export { GEMINI_PROVIDER, THOUGHT_SIGNATURE_KIND };
|
|
164
|
+
/** providerState kind for a genai part the contract cannot hold exactly; the payload is the part. */
|
|
165
|
+
export const GENAI_PART_KIND = 'genai_part';
|
|
166
|
+
/** The prefix of an id the mapping made for a call or result that had none (defined in lib/models/geminiState.ts). */
|
|
167
|
+
export { MINTED_CALL_ID_PREFIX };
|
|
168
|
+
const ALLOWED = {
|
|
169
|
+
system: new Set(['text']),
|
|
170
|
+
user: new Set(['text', 'blob']),
|
|
171
|
+
assistant: new Set(['text', 'thinking', 'toolCall', 'blob']),
|
|
172
|
+
tool: new Set(['toolResult']),
|
|
173
|
+
};
|
|
174
|
+
const GENAI_ROLE = { system: 'system', user: 'user', assistant: 'model', tool: 'user' };
|
|
175
|
+
function isObject(value) {
|
|
176
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
177
|
+
}
|
|
178
|
+
/** The keys that carry a value: JSON drops undefined, so the mapping does too. */
|
|
179
|
+
function keysOf(o) {
|
|
180
|
+
return Object.keys(o).filter((k) => o[k] !== undefined);
|
|
181
|
+
}
|
|
182
|
+
function hasOnly(o, allowed) {
|
|
183
|
+
return keysOf(o).every((k) => allowed.includes(k));
|
|
184
|
+
}
|
|
185
|
+
export function isMintedCallId(id) {
|
|
186
|
+
return typeof id === 'string' && id.startsWith(MINTED_CALL_ID_PREFIX);
|
|
187
|
+
}
|
|
188
|
+
/** An id that may pass through as it is: absent, or a string the mapping did not make. */
|
|
189
|
+
function isOwnId(id) {
|
|
190
|
+
return id === undefined || (typeof id === 'string' && !isMintedCallId(id));
|
|
191
|
+
}
|
|
192
|
+
function json(value) {
|
|
193
|
+
try {
|
|
194
|
+
return JSON.stringify(value) ?? String(value);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return String(value);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function slotOf(content) {
|
|
201
|
+
if (content.role === 'model')
|
|
202
|
+
return 'assistant';
|
|
203
|
+
if (content.role === 'system')
|
|
204
|
+
return 'system';
|
|
205
|
+
const parts = content.parts ?? [];
|
|
206
|
+
if (content.role === 'user' && parts.length > 0 && parts.every((p) => isObject(p) && isObject(p.functionResponse))) {
|
|
207
|
+
return 'tool';
|
|
208
|
+
}
|
|
209
|
+
return 'user';
|
|
210
|
+
}
|
|
211
|
+
/** A response as a result: see the header for the rule and its one ambiguity. */
|
|
212
|
+
function resultOf(response) {
|
|
213
|
+
const keys = keysOf(response);
|
|
214
|
+
if (keys.length === 1 && keys[0] === 'error' && response.error !== null && response.error !== false) {
|
|
215
|
+
return { result: response.error, isError: true };
|
|
216
|
+
}
|
|
217
|
+
if (keys.length === 1 && keys[0] === 'result' && !isObject(response.result))
|
|
218
|
+
return { result: response.result };
|
|
219
|
+
return { result: response };
|
|
220
|
+
}
|
|
221
|
+
/** The genai part's own data, without its signature and state, as the nearest contract part. */
|
|
222
|
+
function project(rest) {
|
|
223
|
+
const { functionResponse: fr, functionCall: fc, inlineData: inline, fileData: file, text, thought } = rest;
|
|
224
|
+
const keys = keysOf(rest);
|
|
225
|
+
const only = (key) => keys.length === 1 && keys[0] === key;
|
|
226
|
+
const textual = (exact) => ({
|
|
227
|
+
part: thought === true ? { type: 'thinking', text: text } : { type: 'text', text: text },
|
|
228
|
+
exact,
|
|
229
|
+
});
|
|
230
|
+
// Text alone is exact; text beside a call, a result or a blob ranks below them.
|
|
231
|
+
if (typeof text === 'string' && hasOnly(rest, ['text', 'thought']) && (thought === undefined || thought === true))
|
|
232
|
+
return textual(true);
|
|
233
|
+
if (isObject(fr)) {
|
|
234
|
+
const exact = only('functionResponse') && hasOnly(fr, ['id', 'name', 'response']) && typeof fr.name === 'string' && isObject(fr.response) && isOwnId(fr.id);
|
|
235
|
+
const r = isObject(fr.response) ? resultOf(fr.response) : { result: fr.response ?? null };
|
|
236
|
+
const id = typeof fr.id === 'string' && !isMintedCallId(fr.id) ? fr.id : undefined;
|
|
237
|
+
return {
|
|
238
|
+
part: { type: 'toolResult', id: id ?? '', name: typeof fr.name === 'string' ? fr.name : '', ...r },
|
|
239
|
+
exact,
|
|
240
|
+
needsId: id === undefined,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (isObject(fc)) {
|
|
244
|
+
const exact = only('functionCall') && hasOnly(fc, ['name', 'args', 'id']) && typeof fc.name === 'string' && isObject(fc.args) && isOwnId(fc.id);
|
|
245
|
+
const id = typeof fc.id === 'string' && !isMintedCallId(fc.id) ? fc.id : undefined;
|
|
246
|
+
return {
|
|
247
|
+
part: { type: 'toolCall', id: id ?? '', name: typeof fc.name === 'string' ? fc.name : '', args: isObject(fc.args) ? fc.args : {} },
|
|
248
|
+
exact,
|
|
249
|
+
needsId: id === undefined,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (isObject(inline) && typeof inline.data === 'string') {
|
|
253
|
+
const exact = only('inlineData') && hasOnly(inline, ['mimeType', 'data']) && typeof inline.mimeType === 'string';
|
|
254
|
+
const mimeType = typeof inline.mimeType === 'string' ? inline.mimeType : 'application/octet-stream';
|
|
255
|
+
return { part: { type: 'blob', mimeType, data: inline.data }, exact };
|
|
256
|
+
}
|
|
257
|
+
if (isObject(file) && typeof file.fileUri === 'string') {
|
|
258
|
+
const exact = only('fileData') && hasOnly(file, ['mimeType', 'fileUri']) && typeof file.mimeType === 'string';
|
|
259
|
+
const mimeType = typeof file.mimeType === 'string' ? file.mimeType : 'application/octet-stream';
|
|
260
|
+
return { part: { type: 'blob', mimeType, url: file.fileUri }, exact };
|
|
261
|
+
}
|
|
262
|
+
if (typeof text === 'string')
|
|
263
|
+
return textual(false);
|
|
264
|
+
if (isObject(rest.executableCode)) {
|
|
265
|
+
const { code, language } = rest.executableCode;
|
|
266
|
+
const lang = typeof language === 'string' && language !== 'LANGUAGE_UNSPECIFIED' ? language.toLowerCase() : '';
|
|
267
|
+
return { part: { type: 'text', text: `\`\`\`${lang}\n${typeof code === 'string' ? code : ''}\n\`\`\`` }, exact: false };
|
|
268
|
+
}
|
|
269
|
+
if (isObject(rest.codeExecutionResult)) {
|
|
270
|
+
const { outcome, output } = rest.codeExecutionResult;
|
|
271
|
+
const failed = typeof outcome === 'string' && outcome !== 'OUTCOME_OK' ? ` (${outcome})` : '';
|
|
272
|
+
return { part: { type: 'text', text: `Output${failed}:\n${typeof output === 'string' ? output : ''}` }, exact: false };
|
|
273
|
+
}
|
|
274
|
+
return { part: { type: 'text', text: '' }, exact: false };
|
|
275
|
+
}
|
|
276
|
+
/** Text standing in for a part its message cannot hold. */
|
|
277
|
+
function describe(part) {
|
|
278
|
+
switch (part.type) {
|
|
279
|
+
case 'text':
|
|
280
|
+
case 'thinking':
|
|
281
|
+
return part.text;
|
|
282
|
+
case 'toolCall':
|
|
283
|
+
return `[${part.name} called with ${json(part.args)}]`;
|
|
284
|
+
case 'toolResult':
|
|
285
|
+
return `[${part.name} ${part.isError ? 'failed' : 'returned'} ${json(part.result)}]`;
|
|
286
|
+
case 'blob':
|
|
287
|
+
return `[${part.mimeType}${part.url !== undefined ? ` ${part.url}` : ''}]`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Another adapter's state, which may ride through as it is. The mapping's
|
|
292
|
+
* own kinds are not: back, they would be read as a signature or a part.
|
|
293
|
+
*/
|
|
294
|
+
function isPassThroughState(state) {
|
|
295
|
+
if (!isObject(state) || typeof state.provider !== 'string' || typeof state.kind !== 'string')
|
|
296
|
+
return false;
|
|
297
|
+
if (state.model !== undefined && typeof state.model !== 'string')
|
|
298
|
+
return false;
|
|
299
|
+
return !(state.provider === GEMINI_PROVIDER && (state.kind === THOUGHT_SIGNATURE_KIND || state.kind === GENAI_PART_KIND));
|
|
300
|
+
}
|
|
301
|
+
function withState(part, state) {
|
|
302
|
+
return { ...part, providerState: state };
|
|
303
|
+
}
|
|
304
|
+
function partFromGenai(raw, slot, index, ids) {
|
|
305
|
+
if (!isObject(raw)) {
|
|
306
|
+
const part = { type: 'text', text: typeof raw === 'string' ? raw : '' };
|
|
307
|
+
return withState(part, { provider: GEMINI_PROVIDER, kind: GENAI_PART_KIND, payload: raw });
|
|
308
|
+
}
|
|
309
|
+
const { thoughtSignature, providerState, ...rest } = raw;
|
|
310
|
+
const projection = project(rest);
|
|
311
|
+
let { part, exact } = projection;
|
|
312
|
+
if (!ALLOWED[slot].has(part.type)) {
|
|
313
|
+
part = { type: 'text', text: describe(part) };
|
|
314
|
+
exact = false;
|
|
315
|
+
}
|
|
316
|
+
else if (projection.needsId && part.type === 'toolCall') {
|
|
317
|
+
part = { ...part, id: ids.call(part.name, index) };
|
|
318
|
+
}
|
|
319
|
+
else if (projection.needsId && part.type === 'toolResult') {
|
|
320
|
+
part = { ...part, id: ids.result(part.name, index) };
|
|
321
|
+
}
|
|
322
|
+
if (exact && thoughtSignature === undefined && providerState === undefined)
|
|
323
|
+
return part;
|
|
324
|
+
if (exact && typeof thoughtSignature === 'string' && providerState === undefined) {
|
|
325
|
+
return withState(part, { provider: GEMINI_PROVIDER, kind: THOUGHT_SIGNATURE_KIND, payload: thoughtSignature });
|
|
326
|
+
}
|
|
327
|
+
if (exact && thoughtSignature === undefined && isPassThroughState(providerState))
|
|
328
|
+
return withState(part, providerState);
|
|
329
|
+
return withState(part, { provider: GEMINI_PROVIDER, kind: GENAI_PART_KIND, payload: raw });
|
|
330
|
+
}
|
|
331
|
+
function idContext(content, open) {
|
|
332
|
+
const mint = (part) => `${MINTED_CALL_ID_PREFIX}${content}-${part}`;
|
|
333
|
+
return {
|
|
334
|
+
call(name, part) {
|
|
335
|
+
const id = mint(part);
|
|
336
|
+
open.push({ name, id, content });
|
|
337
|
+
return id;
|
|
338
|
+
},
|
|
339
|
+
result(name, part) {
|
|
340
|
+
// The latest content's calls first, in their order: parallel calls to
|
|
341
|
+
// one tool are answered in the order they were made.
|
|
342
|
+
let match = -1;
|
|
343
|
+
for (let i = 0; i < open.length; i++) {
|
|
344
|
+
if (open[i].name === name && (match < 0 || open[i].content > open[match].content))
|
|
345
|
+
match = i;
|
|
346
|
+
}
|
|
347
|
+
if (match < 0)
|
|
348
|
+
return mint(part);
|
|
349
|
+
return open.splice(match, 1)[0].id;
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function mapContent(content, index, open) {
|
|
354
|
+
const slot = slotOf(content);
|
|
355
|
+
const ids = idContext(index, open);
|
|
356
|
+
const parts = (content.parts ?? []).map((p, i) => partFromGenai(p, slot, i, ids));
|
|
357
|
+
return { role: slot, parts };
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* One genai content as a contract message. A call or result without an id
|
|
361
|
+
* gets one minted from `index` (the content's position in its history);
|
|
362
|
+
* results are paired with calls only by contentsToMessages.
|
|
363
|
+
*/
|
|
364
|
+
export function contentToMessage(content, index = 0) {
|
|
365
|
+
return mapContent(content, index, []);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* The text of a system instruction in any of genai's spellings, parts joined
|
|
369
|
+
* by newlines, or undefined when it has none (an empty string included).
|
|
370
|
+
*/
|
|
371
|
+
export function systemText(instruction) {
|
|
372
|
+
if (instruction === undefined || instruction === null)
|
|
373
|
+
return undefined;
|
|
374
|
+
if (typeof instruction === 'string')
|
|
375
|
+
return instruction || undefined;
|
|
376
|
+
const parts = Array.isArray(instruction)
|
|
377
|
+
? instruction
|
|
378
|
+
: isObject(instruction) && Array.isArray(instruction.parts)
|
|
379
|
+
? instruction.parts
|
|
380
|
+
: [instruction];
|
|
381
|
+
const texts = parts
|
|
382
|
+
.map((p) => (typeof p === 'string' ? p : isObject(p) && typeof p.text === 'string' && p.thought !== true ? p.text : undefined))
|
|
383
|
+
.filter((t) => t !== undefined);
|
|
384
|
+
return texts.length > 0 ? texts.join('\n') : undefined;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* A genai history (contents plus an optional system instruction) as the
|
|
388
|
+
* contract holds it. Calls and results without ids are paired here.
|
|
389
|
+
*/
|
|
390
|
+
export function contentsToMessages(contents, systemInstruction) {
|
|
391
|
+
const open = [];
|
|
392
|
+
const messages = contents.map((content, index) => mapContent(content, index, open));
|
|
393
|
+
const system = systemText(systemInstruction);
|
|
394
|
+
return system === undefined ? { messages } : { system, messages };
|
|
395
|
+
}
|
|
396
|
+
// ── Contract → genai ─────────────────────────────────────────────────────────
|
|
397
|
+
/** A tool result as a genai `response`: see the header. */
|
|
398
|
+
function responseOf(result, isError) {
|
|
399
|
+
if (isError)
|
|
400
|
+
return { error: result };
|
|
401
|
+
return isObject(result) ? result : { result };
|
|
402
|
+
}
|
|
403
|
+
/** One contract part as the genai part it maps to. */
|
|
404
|
+
export function partToGenai(part) {
|
|
405
|
+
const state = part.providerState;
|
|
406
|
+
if (state?.provider === GEMINI_PROVIDER && state.kind === GENAI_PART_KIND)
|
|
407
|
+
return state.payload;
|
|
408
|
+
let out;
|
|
409
|
+
switch (part.type) {
|
|
410
|
+
case 'text':
|
|
411
|
+
out = { text: part.text };
|
|
412
|
+
break;
|
|
413
|
+
case 'thinking':
|
|
414
|
+
out = { text: part.text, thought: true };
|
|
415
|
+
break;
|
|
416
|
+
case 'toolCall':
|
|
417
|
+
out = { functionCall: { name: part.name, args: part.args, ...(isMintedCallId(part.id) ? {} : { id: part.id }) } };
|
|
418
|
+
break;
|
|
419
|
+
case 'toolResult':
|
|
420
|
+
out = { functionResponse: { ...(isMintedCallId(part.id) ? {} : { id: part.id }), name: part.name, response: responseOf(part.result, part.isError) } };
|
|
421
|
+
break;
|
|
422
|
+
case 'blob':
|
|
423
|
+
out = part.url !== undefined ? { fileData: { mimeType: part.mimeType, fileUri: part.url } } : { inlineData: { mimeType: part.mimeType, data: part.data } };
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
if (state?.provider === GEMINI_PROVIDER && state.kind === THOUGHT_SIGNATURE_KIND && typeof state.payload === 'string') {
|
|
427
|
+
out.thoughtSignature = state.payload;
|
|
428
|
+
}
|
|
429
|
+
else if (state !== undefined) {
|
|
430
|
+
out.providerState = state;
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
/** One contract message as a genai content. */
|
|
435
|
+
export function messageToContent(message) {
|
|
436
|
+
return { role: GENAI_ROLE[message.role], parts: message.parts.map(partToGenai) };
|
|
437
|
+
}
|
|
438
|
+
/** The inverse of contentsToMessages; the system instruction comes back as a string. */
|
|
439
|
+
export function messagesToContents(history) {
|
|
440
|
+
const contents = history.messages.map(messageToContent);
|
|
441
|
+
return history.system === undefined ? { contents } : { systemInstruction: history.system, contents };
|
|
442
|
+
}
|
|
443
|
+
// Maps, not object literals: a config value such as `constructor` must read as nothing.
|
|
444
|
+
const THINKING_LEVELS = new Map([['MINIMAL', 'none'], ['LOW', 'low'], ['MEDIUM', 'medium'], ['HIGH', 'high']]);
|
|
445
|
+
const EFFORT_LEVELS = new Map([['none', 'none'], ['minimal', 'none'], ['low', 'low'], ['medium', 'medium'], ['high', 'high']]);
|
|
446
|
+
/**
|
|
447
|
+
* The ReasoningSetting a generateContentConfig asks for, where one is exact:
|
|
448
|
+
* - `thinkingConfig.thinkingLevel`: `MINIMAL` is `none` (ADR 0047's
|
|
449
|
+
* Gemini 3 rendering of it), `LOW`, `MEDIUM`, `HIGH` their levels.
|
|
450
|
+
* - else `thinkingConfig.thinkingBudget` n ≥ 0: `{ budget_tokens: n }`,
|
|
451
|
+
* which every adapter maps to what the budget maps to today (0 included).
|
|
452
|
+
* - else `reasoningEffort`: a level word is that level, and `minimal` is
|
|
453
|
+
* `none` (ADR 0047's rendering of it on the first GPT-5 generation).
|
|
454
|
+
* Lossy, and absent: a budget of -1 (Gemini's dynamic thinking, which is
|
|
455
|
+
* the provider's default), `THINKING_LEVEL_UNSPECIFIED`, the effort words
|
|
456
|
+
* `xhigh` and `max`, and `includeThoughts`. The compiler writes the effort
|
|
457
|
+
* word beside thinkingConfig from one setting, so preferring thinkingConfig
|
|
458
|
+
* loses nothing it wrote; only the older spelling can set the two apart.
|
|
459
|
+
*/
|
|
460
|
+
export function reasoningOf(config) {
|
|
461
|
+
const cfg = (config ?? {});
|
|
462
|
+
const thinking = cfg.thinkingConfig;
|
|
463
|
+
if (isObject(thinking)) {
|
|
464
|
+
const level = THINKING_LEVELS.get(thinking.thinkingLevel);
|
|
465
|
+
if (level)
|
|
466
|
+
return level;
|
|
467
|
+
const budget = thinking.thinkingBudget;
|
|
468
|
+
if (typeof budget === 'number' && Number.isInteger(budget) && budget >= 0)
|
|
469
|
+
return { budget_tokens: budget };
|
|
470
|
+
}
|
|
471
|
+
return EFFORT_LEVELS.get(cfg.reasoningEffort);
|
|
472
|
+
}
|
|
473
|
+
function nativeToolsOf(llmRequest) {
|
|
474
|
+
const tools = new Set();
|
|
475
|
+
// The sentinels a non-Gemini request carries in toolsDict (web_search,
|
|
476
|
+
// x_search, collections_search): tools that declare no function.
|
|
477
|
+
for (const tool of Object.values(llmRequest.toolsDict ?? {})) {
|
|
478
|
+
const native = nativeToolOf(tool);
|
|
479
|
+
if (native)
|
|
480
|
+
tools.add(native);
|
|
481
|
+
}
|
|
482
|
+
// Gemini's own tool objects. `google_search` and `web_search` both become
|
|
483
|
+
// `{ googleSearch: {} }` on Gemini, so the mapping reads it as the neutral name.
|
|
484
|
+
for (const entry of (llmRequest.config?.tools ?? [])) {
|
|
485
|
+
if (!isObject(entry))
|
|
486
|
+
continue;
|
|
487
|
+
if (entry.googleSearch !== undefined || entry.googleSearchRetrieval !== undefined)
|
|
488
|
+
tools.add('web_search');
|
|
489
|
+
if (entry.urlContext !== undefined)
|
|
490
|
+
tools.add('url_context');
|
|
491
|
+
if (entry.codeExecution !== undefined)
|
|
492
|
+
tools.add('code_execution');
|
|
493
|
+
}
|
|
494
|
+
return [...tools];
|
|
495
|
+
}
|
|
496
|
+
/** The function-calling mode as a ToolChoice, and VALIDATED as strict tools. The native request builder reads an agent's config through it too. */
|
|
497
|
+
export function toolChoiceOf(config) {
|
|
498
|
+
const fcc = config?.toolConfig?.functionCallingConfig;
|
|
499
|
+
if (!isObject(fcc))
|
|
500
|
+
return {};
|
|
501
|
+
const allowed = Array.isArray(fcc.allowedFunctionNames) ? fcc.allowedFunctionNames : [];
|
|
502
|
+
switch (String(fcc.mode)) {
|
|
503
|
+
case 'NONE':
|
|
504
|
+
return { toolChoice: 'none' };
|
|
505
|
+
case 'ANY':
|
|
506
|
+
// Several allowed names have no contract form: any tool is required.
|
|
507
|
+
return { toolChoice: allowed.length === 1 && typeof allowed[0] === 'string' ? { name: allowed[0] } : 'required' };
|
|
508
|
+
case 'VALIDATED':
|
|
509
|
+
return { strict: true };
|
|
510
|
+
default:
|
|
511
|
+
return {};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/** The sampling fields the contract carries, or undefined when none is set. The native request builder reads an agent's config through it too. */
|
|
515
|
+
export function samplingOf(config) {
|
|
516
|
+
const sampling = {};
|
|
517
|
+
if (typeof config?.temperature === 'number')
|
|
518
|
+
sampling.temperature = config.temperature;
|
|
519
|
+
if (typeof config?.topP === 'number')
|
|
520
|
+
sampling.topP = config.topP;
|
|
521
|
+
if (typeof config?.maxOutputTokens === 'number')
|
|
522
|
+
sampling.maxOutputTokens = config.maxOutputTokens;
|
|
523
|
+
if (Array.isArray(config?.stopSequences) && config.stopSequences.length > 0)
|
|
524
|
+
sampling.stop = [...config.stopSequences];
|
|
525
|
+
return Object.keys(sampling).length > 0 ? sampling : undefined;
|
|
526
|
+
}
|
|
527
|
+
/** An ADK LlmRequest as a ModelRequest (see the header for what is not mapped). */
|
|
528
|
+
export function llmRequestToModelRequest(llmRequest, options = {}) {
|
|
529
|
+
const model = options.model ?? llmRequest.model;
|
|
530
|
+
if (!model)
|
|
531
|
+
throw new TypeError('llmRequestToModelRequest: the request names no model, and none was given');
|
|
532
|
+
const config = llmRequest.config;
|
|
533
|
+
const { messages } = contentsToMessages(llmRequest.contents ?? []);
|
|
534
|
+
const system = systemText(config?.systemInstruction);
|
|
535
|
+
const { toolChoice, strict } = toolChoiceOf(config);
|
|
536
|
+
// VALIDATED asks Gemini to enforce the schema as declared, so the flag is
|
|
537
|
+
// set on the plain declaration rather than asking for the strict form.
|
|
538
|
+
const tools = [];
|
|
539
|
+
for (const tool of Object.values(llmRequest.toolsDict ?? {})) {
|
|
540
|
+
const declaration = contractToolDeclaration(tool);
|
|
541
|
+
if (declaration)
|
|
542
|
+
tools.push(strict ? { ...declaration, strict } : declaration);
|
|
543
|
+
}
|
|
544
|
+
const nativeTools = nativeToolsOf(llmRequest);
|
|
545
|
+
const schema = config?.responseJsonSchema ?? config?.responseSchema;
|
|
546
|
+
// JSON mode: the MIME type alone, with no schema to say more (ADR 0061).
|
|
547
|
+
const jsonMode = !isObject(schema) && config?.responseMimeType === 'application/json';
|
|
548
|
+
const reasoning = reasoningOf(config);
|
|
549
|
+
const sampling = samplingOf(config);
|
|
550
|
+
const signal = options.signal ?? config?.abortSignal;
|
|
551
|
+
return {
|
|
552
|
+
model,
|
|
553
|
+
...(system !== undefined ? { system } : {}),
|
|
554
|
+
messages,
|
|
555
|
+
...(tools.length > 0 ? { tools } : {}),
|
|
556
|
+
...(nativeTools.length > 0 ? { nativeTools } : {}),
|
|
557
|
+
...(toolChoice !== undefined ? { toolChoice } : {}),
|
|
558
|
+
...(isObject(schema) ? { outputSchema: toContractJsonSchema(schema) } : {}),
|
|
559
|
+
...(jsonMode ? { outputFormat: 'json' } : {}),
|
|
560
|
+
...(reasoning !== undefined ? { reasoning } : {}),
|
|
561
|
+
...(sampling ? { sampling } : {}),
|
|
562
|
+
...(options.stream !== undefined ? { stream: options.stream } : {}),
|
|
563
|
+
...(signal ? { signal } : {}),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
// ── ModelResponse → LlmResponse ──────────────────────────────────────────────
|
|
567
|
+
/**
|
|
568
|
+
* Usage in Gemini's meanings: `candidatesTokenCount` excludes the thinking
|
|
569
|
+
* that `thoughtsTokenCount` counts, and `totalTokenCount` is input plus
|
|
570
|
+
* output. `cacheWriteTokens` has no genai field.
|
|
571
|
+
*/
|
|
572
|
+
export function usageToMetadata(usage) {
|
|
573
|
+
const thinking = usage.thinkingTokens ?? 0;
|
|
574
|
+
return {
|
|
575
|
+
promptTokenCount: usage.inputTokens,
|
|
576
|
+
candidatesTokenCount: Math.max(0, usage.outputTokens - thinking),
|
|
577
|
+
...(usage.thinkingTokens !== undefined ? { thoughtsTokenCount: usage.thinkingTokens } : {}),
|
|
578
|
+
...(usage.cacheReadTokens !== undefined ? { cachedContentTokenCount: usage.cacheReadTokens } : {}),
|
|
579
|
+
totalTokenCount: usage.inputTokens + usage.outputTokens,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
/** Gemini's usageMetadata as contract Usage, or undefined when it counts nothing. */
|
|
583
|
+
export function usageFromMetadata(meta) {
|
|
584
|
+
if (!meta)
|
|
585
|
+
return undefined;
|
|
586
|
+
const counted = [meta.promptTokenCount, meta.toolUsePromptTokenCount, meta.candidatesTokenCount, meta.thoughtsTokenCount];
|
|
587
|
+
if (counted.every((n) => n === undefined))
|
|
588
|
+
return undefined;
|
|
589
|
+
return {
|
|
590
|
+
inputTokens: (meta.promptTokenCount ?? 0) + (meta.toolUsePromptTokenCount ?? 0),
|
|
591
|
+
outputTokens: (meta.candidatesTokenCount ?? 0) + (meta.thoughtsTokenCount ?? 0),
|
|
592
|
+
...(meta.thoughtsTokenCount !== undefined ? { thinkingTokens: meta.thoughtsTokenCount } : {}),
|
|
593
|
+
...(meta.cachedContentTokenCount !== undefined ? { cacheReadTokens: meta.cachedContentTokenCount } : {}),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
/** `content_filter` cannot say which of Gemini's policy reasons it was; it becomes SAFETY. */
|
|
597
|
+
const FINISH_REASONS = {
|
|
598
|
+
stop: FinishReason.STOP,
|
|
599
|
+
tool_call: FinishReason.STOP,
|
|
600
|
+
max_tokens: FinishReason.MAX_TOKENS,
|
|
601
|
+
content_filter: FinishReason.SAFETY,
|
|
602
|
+
other: FinishReason.OTHER,
|
|
603
|
+
error: undefined,
|
|
604
|
+
};
|
|
605
|
+
/** The groundingMetadata lib/grounding.ts reads: the queries, and each cited URL once. */
|
|
606
|
+
function groundingToMetadata(grounding) {
|
|
607
|
+
const webSearchQueries = (grounding.searchQueries ?? []).map((q) => q.query).filter((q) => q);
|
|
608
|
+
const seen = new Set();
|
|
609
|
+
const groundingChunks = [];
|
|
610
|
+
for (const c of grounding.citations ?? []) {
|
|
611
|
+
if (seen.has(c.url))
|
|
612
|
+
continue;
|
|
613
|
+
seen.add(c.url);
|
|
614
|
+
groundingChunks.push({ web: { uri: c.url, ...(c.title !== undefined ? { title: c.title } : {}) } });
|
|
615
|
+
}
|
|
616
|
+
if (webSearchQueries.length === 0 && groundingChunks.length === 0)
|
|
617
|
+
return undefined;
|
|
618
|
+
return {
|
|
619
|
+
...(webSearchQueries.length > 0 ? { webSearchQueries } : {}),
|
|
620
|
+
...(groundingChunks.length > 0 ? { groundingChunks } : {}),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/** Gemini's finish reasons that end a call short of an answer: every one but STOP and the unspecified one. */
|
|
624
|
+
const GEMINI_FINISH_REASONS = new Set(Object.values(FinishReason).filter((r) => r !== FinishReason.STOP && r !== FinishReason.FINISH_REASON_UNSPECIFIED));
|
|
625
|
+
/** An error code that is one of Gemini's finish reasons, as the finish reason ADK's Gemini reports beside it (ADR 0088). */
|
|
626
|
+
function finishReasonOfCode(code) {
|
|
627
|
+
return code !== undefined && GEMINI_FINISH_REASONS.has(code) ? code : undefined;
|
|
628
|
+
}
|
|
629
|
+
/** A ModelResponse as the LlmResponse ADK expects from a model (see the header). */
|
|
630
|
+
export function modelResponseToLlmResponse(response) {
|
|
631
|
+
const parts = response.parts.map(partToGenai);
|
|
632
|
+
if (response.partial)
|
|
633
|
+
return { content: { role: 'model', parts }, partial: true };
|
|
634
|
+
const finishReason = finishReasonOfCode(response.error?.code) ?? FINISH_REASONS[response.finishReason];
|
|
635
|
+
const groundingMetadata = response.grounding ? groundingToMetadata(response.grounding) : undefined;
|
|
636
|
+
const llmResponse = {
|
|
637
|
+
...(parts.length > 0 ? { content: { role: 'model', parts } } : {}),
|
|
638
|
+
...(response.error ? { errorCode: response.error.code, errorMessage: response.error.message } : {}),
|
|
639
|
+
...(finishReason !== undefined ? { finishReason } : {}),
|
|
640
|
+
...(response.usage ? { usageMetadata: usageToMetadata(response.usage) } : {}),
|
|
641
|
+
...(groundingMetadata ? { groundingMetadata } : {}),
|
|
642
|
+
turnComplete: true,
|
|
643
|
+
};
|
|
644
|
+
// FallbackLlm reads the verdict from customMetadata alone (ADR 0044): an
|
|
645
|
+
// error without it would be passed on, never answered by the fallback.
|
|
646
|
+
if (!response.error)
|
|
647
|
+
return llmResponse;
|
|
648
|
+
const { retryable, status } = response.error;
|
|
649
|
+
return withRetryVerdict(llmResponse, { retryable, ...(status !== undefined ? { status } : {}) });
|
|
650
|
+
}
|
|
651
|
+
// ── ModelRequest → LlmRequest ────────────────────────────────────────────────
|
|
652
|
+
/** The Gemini tool object each native tool becomes. `x_search` and `collections_search` have none. */
|
|
653
|
+
const GEMINI_NATIVE_TOOLS = {
|
|
654
|
+
web_search: () => ({ googleSearch: {} }),
|
|
655
|
+
google_search: () => ({ googleSearch: {} }),
|
|
656
|
+
url_context: () => ({ urlContext: {} }),
|
|
657
|
+
code_execution: () => ({ codeExecution: {} }),
|
|
658
|
+
};
|
|
659
|
+
/** The native tools in `tools` that Gemini has no tool for, each once: modelRequestToLlmRequest leaves them out. */
|
|
660
|
+
export function nativeToolsWithoutGeminiTool(tools = []) {
|
|
661
|
+
return [...new Set(tools)].filter((tool) => !GEMINI_NATIVE_TOOLS[tool]);
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* A toolsDict entry that only declares. `_getDeclaration()` gives the
|
|
665
|
+
* declaration with its lowercase schema as `parameters`, where
|
|
666
|
+
* llmRequestToModelRequest (contractToolDeclaration) and the ADK-path
|
|
667
|
+
* adapters (toolDeclarationFor) read it. It is never run: ADK's Gemini sends
|
|
668
|
+
* `config.tools`, and nothing calls a tool out of a request.
|
|
669
|
+
*/
|
|
670
|
+
function declaredTool({ name, description, parameters }) {
|
|
671
|
+
return { name, description, isLongRunning: false, _getDeclaration: () => ({ name, description, parameters }) };
|
|
672
|
+
}
|
|
673
|
+
/** The function-calling mode for the request's tool choice and strict tools, sent only beside declarations. */
|
|
674
|
+
function toolConfigOf(request) {
|
|
675
|
+
if (!request.tools?.length)
|
|
676
|
+
return undefined;
|
|
677
|
+
const choice = request.toolChoice;
|
|
678
|
+
if (typeof choice === 'object') {
|
|
679
|
+
return { functionCallingConfig: { mode: FunctionCallingConfigMode.ANY, allowedFunctionNames: [choice.name] } };
|
|
680
|
+
}
|
|
681
|
+
const mode = choice === 'none'
|
|
682
|
+
? FunctionCallingConfigMode.NONE
|
|
683
|
+
: choice === 'required'
|
|
684
|
+
? FunctionCallingConfigMode.ANY
|
|
685
|
+
: request.tools.some((t) => t.strict)
|
|
686
|
+
? FunctionCallingConfigMode.VALIDATED
|
|
687
|
+
: choice === 'auto'
|
|
688
|
+
? FunctionCallingConfigMode.AUTO
|
|
689
|
+
: undefined;
|
|
690
|
+
return mode ? { functionCallingConfig: { mode } } : undefined;
|
|
691
|
+
}
|
|
692
|
+
/** A ModelRequest as the LlmRequest ADK's path builds for a Gemini model (see the header for what does not come back the same). */
|
|
693
|
+
export function modelRequestToLlmRequest(request) {
|
|
694
|
+
const config = {};
|
|
695
|
+
if (request.system)
|
|
696
|
+
config.systemInstruction = request.system;
|
|
697
|
+
const declarations = request.tools ?? [];
|
|
698
|
+
const tools = [];
|
|
699
|
+
if (declarations.length > 0) {
|
|
700
|
+
tools.push({
|
|
701
|
+
functionDeclarations: declarations.map(({ name, description, parameters }) => ({ name, description, parametersJsonSchema: parameters })),
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
// In the request's order, so llmRequestToModelRequest reads them back in it.
|
|
705
|
+
let searching = false;
|
|
706
|
+
for (const native of new Set(request.nativeTools ?? [])) {
|
|
707
|
+
const tool = GEMINI_NATIVE_TOOLS[native];
|
|
708
|
+
if (!tool)
|
|
709
|
+
continue;
|
|
710
|
+
if (native === 'web_search' || native === 'google_search') {
|
|
711
|
+
if (searching)
|
|
712
|
+
continue;
|
|
713
|
+
searching = true;
|
|
714
|
+
}
|
|
715
|
+
tools.push(tool());
|
|
716
|
+
}
|
|
717
|
+
if (tools.length > 0)
|
|
718
|
+
config.tools = tools;
|
|
719
|
+
const toolConfig = toolConfigOf(request);
|
|
720
|
+
if (toolConfig)
|
|
721
|
+
config.toolConfig = toolConfig;
|
|
722
|
+
if (request.outputSchema) {
|
|
723
|
+
config.responseMimeType = 'application/json';
|
|
724
|
+
config.responseJsonSchema = request.outputSchema;
|
|
725
|
+
}
|
|
726
|
+
else if (request.outputFormat === 'json') {
|
|
727
|
+
config.responseMimeType = 'application/json';
|
|
728
|
+
}
|
|
729
|
+
// The fields the compiler writes for the model (ADR 0047): a thinking
|
|
730
|
+
// level or budget on Gemini, and the effort word every other adapter reads.
|
|
731
|
+
if (request.reasoning !== undefined)
|
|
732
|
+
Object.assign(config, reasoningConfig(request.model, request.reasoning));
|
|
733
|
+
const sampling = request.sampling;
|
|
734
|
+
if (sampling?.temperature !== undefined)
|
|
735
|
+
config.temperature = sampling.temperature;
|
|
736
|
+
if (sampling?.topP !== undefined)
|
|
737
|
+
config.topP = sampling.topP;
|
|
738
|
+
if (sampling?.maxOutputTokens !== undefined)
|
|
739
|
+
config.maxOutputTokens = sampling.maxOutputTokens;
|
|
740
|
+
if (sampling?.stop?.length)
|
|
741
|
+
config.stopSequences = [...sampling.stop];
|
|
742
|
+
if (request.signal)
|
|
743
|
+
config.abortSignal = request.signal;
|
|
744
|
+
return {
|
|
745
|
+
model: request.model,
|
|
746
|
+
contents: messagesToContents({ messages: request.messages }).contents,
|
|
747
|
+
config,
|
|
748
|
+
liveConnectConfig: {},
|
|
749
|
+
toolsDict: Object.fromEntries(declarations.map((d) => [d.name, declaredTool(d)])),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
/** Gemini's reasons for withholding an answer, or blocking a prompt, on policy grounds. */
|
|
753
|
+
const POLICY_REASONS = new Set(['SAFETY', 'RECITATION', 'BLOCKLIST', 'PROHIBITED_CONTENT', 'SPII', 'MODEL_ARMOR', 'JAILBREAK']);
|
|
754
|
+
function isPolicyReason(reason) {
|
|
755
|
+
return POLICY_REASONS.has(reason) || reason.startsWith('IMAGE_');
|
|
756
|
+
}
|
|
757
|
+
/** Gemini's finish reason as the contract's (the Gemini table of wiki/models/model-contract.md). */
|
|
758
|
+
function finishReasonOf(reason) {
|
|
759
|
+
if (reason === 'STOP')
|
|
760
|
+
return 'stop';
|
|
761
|
+
if (reason === 'MAX_TOKENS')
|
|
762
|
+
return 'max_tokens';
|
|
763
|
+
return isPolicyReason(reason) ? 'content_filter' : 'other';
|
|
764
|
+
}
|
|
765
|
+
function isSignatureState(state) {
|
|
766
|
+
return state?.provider === GEMINI_PROVIDER && state.kind === THOUGHT_SIGNATURE_KIND;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* An assistant message's parts as a final's: thinking left out, a signature
|
|
770
|
+
* it carried moved to the next part when that part has no state of its own,
|
|
771
|
+
* or kept by the last part when no part follows; `model` set on every
|
|
772
|
+
* signature that names none.
|
|
773
|
+
*/
|
|
774
|
+
function outputPartsOf(parts, model) {
|
|
775
|
+
const out = [];
|
|
776
|
+
let carried;
|
|
777
|
+
for (const part of parts) {
|
|
778
|
+
if (part.type === 'thinking') {
|
|
779
|
+
if (isSignatureState(part.providerState))
|
|
780
|
+
carried = part.providerState;
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
if (part.type === 'toolResult')
|
|
784
|
+
continue; // an assistant message holds none
|
|
785
|
+
out.push(carried && part.providerState === undefined ? { ...part, providerState: carried } : part);
|
|
786
|
+
carried = undefined;
|
|
787
|
+
}
|
|
788
|
+
const last = out.at(-1);
|
|
789
|
+
if (carried && last && last.providerState === undefined)
|
|
790
|
+
out[out.length - 1] = { ...last, providerState: carried };
|
|
791
|
+
if (model === undefined)
|
|
792
|
+
return out;
|
|
793
|
+
return out.map((p) => (isSignatureState(p.providerState) && p.providerState.model === undefined ? { ...p, providerState: { ...p.providerState, model } } : p));
|
|
794
|
+
}
|
|
795
|
+
/** An ADK error code as the contract's error, with the verdict the adapter stamped (withRetryVerdict). `STOP` is no error. */
|
|
796
|
+
function errorOf(response) {
|
|
797
|
+
const code = response.errorCode;
|
|
798
|
+
if (!code || code === 'STOP')
|
|
799
|
+
return undefined;
|
|
800
|
+
const status = response.customMetadata?.[ERROR_STATUS_KEY];
|
|
801
|
+
return {
|
|
802
|
+
code,
|
|
803
|
+
message: errorText(response.errorMessage || `The model call ended with ${code}.`),
|
|
804
|
+
retryable: response.customMetadata?.[ERROR_RETRYABLE_KEY] === true,
|
|
805
|
+
...(typeof status === 'number' ? { status } : {}),
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
/** Each cited page once, with its title, and the queries run: what groundingToMetadata writes. Citation spans are not read. */
|
|
809
|
+
function groundingOf(meta, tool) {
|
|
810
|
+
if (!meta)
|
|
811
|
+
return undefined;
|
|
812
|
+
const citations = [];
|
|
813
|
+
const seen = new Set();
|
|
814
|
+
for (const chunk of meta.groundingChunks ?? []) {
|
|
815
|
+
const url = chunk.web?.uri;
|
|
816
|
+
if (!url || seen.has(url))
|
|
817
|
+
continue;
|
|
818
|
+
seen.add(url);
|
|
819
|
+
citations.push(chunk.web?.title ? { url, title: chunk.web.title } : { url });
|
|
820
|
+
}
|
|
821
|
+
const searchQueries = (meta.webSearchQueries ?? []).filter((q) => typeof q === 'string' && q).map((query) => ({ tool, query }));
|
|
822
|
+
if (citations.length === 0 && searchQueries.length === 0)
|
|
823
|
+
return undefined;
|
|
824
|
+
return { ...(citations.length > 0 ? { citations } : {}), ...(searchQueries.length > 0 ? { searchQueries } : {}) };
|
|
825
|
+
}
|
|
826
|
+
/** One LlmResponse as a ModelResponse (see the header). A caller folding a stream of them into one final keeps the thinking itself. */
|
|
827
|
+
export function llmResponseToModelResponse(response, options = {}) {
|
|
828
|
+
const raw = response.content?.parts ?? [];
|
|
829
|
+
if (response.partial === true) {
|
|
830
|
+
const parts = [];
|
|
831
|
+
for (const p of raw) {
|
|
832
|
+
if (!isObject(p) || typeof p.text !== 'string' || p.text === '')
|
|
833
|
+
continue;
|
|
834
|
+
parts.push(p.thought === true ? { type: 'thinking', text: p.text } : { type: 'text', text: p.text });
|
|
835
|
+
}
|
|
836
|
+
return { partial: true, parts };
|
|
837
|
+
}
|
|
838
|
+
const message = contentToMessage({ role: 'model', parts: raw }, options.index ?? 0);
|
|
839
|
+
const parts = outputPartsOf(message.parts, options.model);
|
|
840
|
+
const error = errorOf(response);
|
|
841
|
+
const reason = typeof response.finishReason === 'string' && response.finishReason ? response.finishReason : undefined;
|
|
842
|
+
const finishReason = parts.some((p) => p.type === 'toolCall')
|
|
843
|
+
? 'tool_call'
|
|
844
|
+
: reason
|
|
845
|
+
? finishReasonOf(reason)
|
|
846
|
+
: error
|
|
847
|
+
? isPolicyReason(error.code)
|
|
848
|
+
? 'content_filter'
|
|
849
|
+
: 'error'
|
|
850
|
+
: 'stop';
|
|
851
|
+
const usage = usageFromMetadata(response.usageMetadata);
|
|
852
|
+
const grounding = groundingOf(response.groundingMetadata, options.searchTool ?? 'web_search');
|
|
853
|
+
const final = {
|
|
854
|
+
partial: false,
|
|
855
|
+
parts,
|
|
856
|
+
finishReason,
|
|
857
|
+
...(usage ? { usage } : {}),
|
|
858
|
+
...(grounding ? { grounding } : {}),
|
|
859
|
+
...(error ? { error } : {}),
|
|
860
|
+
};
|
|
861
|
+
return final;
|
|
862
|
+
}
|