melchizedek-agents 0.18.0 → 0.20.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 +1001 -0
- package/DOCUMENTATION.md +252 -65
- package/QUICKSTART.md +9 -6
- package/README.md +42 -4
- package/config/agents/examples/harness.yaml +3 -1
- package/config/agents/examples/model_zoo.yaml +10 -9
- package/config/agents/syndicateSchema.yaml +53 -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/adkPeer.d.ts +63 -0
- package/dist/lib/adkPeer.js +240 -0
- package/dist/lib/compile.d.ts +159 -30
- package/dist/lib/compile.js +215 -134
- package/dist/lib/compileAdk.d.ts +32 -0
- package/dist/lib/compileAdk.js +142 -0
- package/dist/lib/compileNative.d.ts +111 -0
- package/dist/lib/compileNative.js +278 -0
- package/dist/lib/config.d.ts +6 -5
- package/dist/lib/config.js +6 -5
- package/dist/lib/doctor.d.ts +28 -1
- package/dist/lib/doctor.js +64 -2
- package/dist/lib/index.d.ts +6 -1
- package/dist/lib/index.js +7 -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 +115 -0
- package/dist/lib/models/adapterResolver.js +137 -0
- package/dist/lib/models/adkGeminiAdapter.d.ts +90 -0
- package/dist/lib/models/adkGeminiAdapter.js +369 -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 +33 -23
- 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 +45 -350
- 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 +18 -11
- package/dist/lib/models/fallback.js +54 -43
- 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 +16 -77
- package/dist/lib/models/geminiAdapter.d.ts +125 -0
- package/dist/lib/models/geminiAdapter.js +854 -0
- package/dist/lib/models/geminiState.d.ts +46 -0
- package/dist/lib/models/geminiState.js +51 -0
- package/dist/lib/models/genaiMapping.d.ts +283 -0
- package/dist/lib/models/genaiMapping.js +965 -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 +89 -603
- 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 +24 -69
- 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 +19 -155
- 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 +15 -153
- 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 +62 -35
- package/dist/lib/models/registry.js +100 -124
- 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/approvals.d.ts +5 -0
- package/dist/lib/runtime/approvals.js +39 -8
- package/dist/lib/runtime/credentials.d.ts +61 -0
- package/dist/lib/runtime/credentials.js +67 -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 +209 -0
- package/dist/lib/runtime/native/agentLoop.js +705 -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 +151 -0
- package/dist/lib/runtime/native/delegate.js +256 -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 +129 -0
- package/dist/lib/runtime/native/interrupts.js +310 -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 +164 -0
- package/dist/lib/runtime/native/selfCorrection.js +327 -0
- package/dist/lib/runtime/native/step.d.ts +156 -0
- package/dist/lib/runtime/native/step.js +263 -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 +38 -0
- package/dist/lib/runtime/runtimeFlag.js +58 -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 +46 -5
- package/dist/lib/runtime/syndicateTurn.js +233 -36
- package/dist/lib/runtime/valueDepth.d.ts +23 -0
- package/dist/lib/runtime/valueDepth.js +41 -0
- package/dist/lib/session/supabaseSessionService.d.ts +65 -18
- package/dist/lib/session/supabaseSessionService.js +148 -105
- package/dist/lib/session/transcript.d.ts +69 -23
- package/dist/lib/session/transcript.js +105 -29
- 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 +44 -16
- package/dist/lib/storage/postgres/sessionService.js +128 -66
- package/dist/lib/syndicateSchema.d.ts +49 -0
- package/dist/lib/syndicateSchema.js +99 -9
- package/dist/lib/toolRegistry.d.ts +26 -2
- package/dist/lib/toolRegistry.js +73 -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 +8 -7
- package/dist/lib/tools/collectionsSearchTool.js +12 -27
- 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 +4 -1
- package/dist/lib/tools/urlContextTool.js +6 -2
- package/dist/lib/tools/webExtractTool.d.ts +3 -3
- package/dist/lib/tools/webExtractTool.js +2 -1
- package/dist/lib/tools/webSearchTool.d.ts +7 -10
- package/dist/lib/tools/webSearchTool.js +15 -60
- package/dist/lib/tools/wikiTools.d.ts +19 -19
- package/dist/lib/tools/wikiTools.js +13 -9
- package/dist/lib/tools/xApiSearchTool.d.ts +3 -3
- package/dist/lib/tools/xApiSearchTool.js +2 -1
- package/dist/lib/tools/xSearchTool.d.ts +8 -10
- package/dist/lib/tools/xSearchTool.js +12 -63
- 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 +74 -35
- package/dist/lib/wiki/entities.d.ts +12 -0
- package/dist/lib/wiki/entities.js +45 -3
- package/dist/lib/workflow/agentNode.d.ts +153 -0
- package/dist/lib/workflow/agentNode.js +365 -0
- package/dist/lib/workflow/graph.d.ts +178 -0
- package/dist/lib/workflow/graph.js +468 -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 +166 -0
- package/dist/lib/workflow/resume.js +441 -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 +418 -0
- package/dist/lib/workflow/scheduler.js +756 -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/workflow.d.ts +33 -11
- package/dist/lib/workflow.js +49 -27
- package/dist/lib/workflowConfig.d.ts +7 -2
- package/dist/lib/workflowConfig.js +7 -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/doctor.d.ts +3 -1
- package/dist/scripts/doctor.js +4 -2
- package/dist/scripts/syndicate_chat.js +19 -10
- package/package.json +9 -3
- 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
|
@@ -1,692 +1,130 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/models/openAiCompatibleLlm.ts —
|
|
3
|
-
*
|
|
2
|
+
* lib/models/openAiCompatibleLlm.ts — the ADK shim base for every
|
|
3
|
+
* chat-completions adapter (Ollama, Kimi, the gateway).
|
|
4
4
|
*
|
|
5
5
|
* WHY this file exists:
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* The chat-completions translation lives on the engine's own model
|
|
7
|
+
* contract, in lib/models/chatCompletionsAdapter.ts and one adapter per
|
|
8
|
+
* provider (ollamaAdapter.ts, kimiAdapter.ts, gatewayAdapter.ts). ADK
|
|
9
|
+
* still runs every turn (ADR 0045), so each adapter runs behind an ADK
|
|
10
|
+
* BaseLlm: OllamaLlm, KimiLlm and GatewayLlm, subclasses of this class,
|
|
11
|
+
* which is an AdkShim (lib/models/adkShim.ts, ADR 0053). The shim charges
|
|
12
|
+
* the turn, opens the llm.request span and maps the request and responses;
|
|
13
|
+
* the adapter does the provider's work.
|
|
14
14
|
*
|
|
15
|
-
* WHAT
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
15
|
+
* WHAT THIS CLASS ADDS TO THE SHIM (ADR 0057): the ADK path stays what it
|
|
16
|
+
* was before the adapters moved onto the contract.
|
|
17
|
+
* - The request: an agent's older generateContentConfig spelling that the
|
|
18
|
+
* contract leaves out rides beside it (olderSpellingOf): an effort word
|
|
19
|
+
* that is no contract level (Kimi K3's `max`). JSON mode without a
|
|
20
|
+
* schema is the contract's `outputFormat` (ADR 0061).
|
|
21
|
+
* - The response: the final keeps the shape these classes always yielded,
|
|
22
|
+
* which the ledger and FallbackLlm read. usageMetadata counts the
|
|
23
|
+
* reasoning inside candidatesTokenCount, as the provider's
|
|
24
|
+
* completion_tokens does, so llm.tokens.output and the turn's charge are
|
|
25
|
+
* unchanged (the shim's default is Gemini's meaning, which excludes it).
|
|
26
|
+
* finishReason is set only for a reply cut short (MAX_TOKENS); a final
|
|
27
|
+
* with no parts keeps its empty content; an HTTP failure keeps its
|
|
28
|
+
* top-level `status` and `retryable` beside the verdict in
|
|
29
|
+
* customMetadata.
|
|
30
|
+
*
|
|
31
|
+
* A /chat/completions provider of your own is a ChatCompletionsAdapter
|
|
32
|
+
* subclass (the endpoint, headers, wire name, reasoning field and error
|
|
33
|
+
* wording), run under ADK as `adkShim(adapter)` or as a subclass of this
|
|
34
|
+
* class.
|
|
31
35
|
*/
|
|
32
|
-
import { BaseLlm } from '@google/adk';
|
|
33
36
|
import { FinishReason } from '@google/genai';
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
37
|
+
import { AdkShim } from './adkShim.js';
|
|
38
|
+
import { setLlmSpanAttribute } from '../observability/tracer.js';
|
|
39
|
+
import { isRetryableStatus } from './retry.js';
|
|
40
|
+
import { statusDecision, withRetryVerdict } from './errorResponse.js';
|
|
41
|
+
export { REASONING_CONTENT_KIND, splitThinkBlocks, ThinkStreamSplitter } from './chatCompletionsAdapter.js';
|
|
42
|
+
/** The effort words a contract ReasoningSetting carries (genaiMapping's reasoningOf); any other rides as the older spelling. */
|
|
43
|
+
const CONTRACT_EFFORT_WORDS = new Set(['none', 'minimal', 'low', 'medium', 'high']);
|
|
41
44
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
45
|
+
* What a generateContentConfig asks of a chat-completions provider that the
|
|
46
|
+
* contract has no field for, or undefined when it asks nothing more:
|
|
47
|
+
* `reasoningEffort` when it is a word no ReasoningSetting carries (`max`,
|
|
48
|
+
* `xhigh`), sent as written. JSON mode without a schema is no longer here:
|
|
49
|
+
* the genai mapping reads it as `outputFormat: 'json'` (ADR 0061).
|
|
47
50
|
*/
|
|
48
|
-
export function
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (unclosed !== -1) {
|
|
56
|
-
const inner = answer.slice(unclosed + OPEN_THINK.length).trim();
|
|
57
|
-
if (inner)
|
|
58
|
-
blocks.push(inner);
|
|
59
|
-
answer = answer.slice(0, unclosed).trimEnd();
|
|
60
|
-
}
|
|
61
|
-
return { reasoning: blocks.join('\n\n'), answer: answer.trimStart() };
|
|
62
|
-
}
|
|
63
|
-
/** Length of the longest suffix of `s` that is a proper prefix of `tag`. */
|
|
64
|
-
function partialTagSuffix(s, tag) {
|
|
65
|
-
const max = Math.min(s.length, tag.length - 1);
|
|
66
|
-
for (let n = max; n > 0; n--) {
|
|
67
|
-
if (s.endsWith(tag.slice(0, n)))
|
|
68
|
-
return n;
|
|
69
|
-
}
|
|
70
|
-
return 0;
|
|
51
|
+
export function olderSpellingOf(config) {
|
|
52
|
+
const cfg = (config ?? {});
|
|
53
|
+
const out = {};
|
|
54
|
+
const effort = cfg.reasoningEffort;
|
|
55
|
+
if (typeof effort === 'string' && !CONTRACT_EFFORT_WORDS.has(effort))
|
|
56
|
+
out.reasoningEffort = effort;
|
|
57
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
71
58
|
}
|
|
72
59
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* `</think>` has usually NOT arrived yet, so the regex matches nothing and
|
|
77
|
-
* the whole scratchpad would be printed as the reply. A tag can also be
|
|
78
|
-
* split mid-delta ("<thi" + "nk>"), so any tail that could still grow into
|
|
79
|
-
* a tag is held back instead of being emitted as answer text.
|
|
80
|
-
*
|
|
81
|
-
* Providers that report reasoning in a discrete field (Ollama's
|
|
82
|
-
* `delta.reasoning`, others' `reasoning_content`) never open a think block
|
|
83
|
-
* and pass straight through this untouched.
|
|
60
|
+
* Usage as the chat-completions classes have always reported it on the ADK
|
|
61
|
+
* path: candidatesTokenCount is the provider's completion_tokens, reasoning
|
|
62
|
+
* included, and thoughtsTokenCount the reasoning part of it.
|
|
84
63
|
*/
|
|
85
|
-
export
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
let answer = '';
|
|
93
|
-
for (;;) {
|
|
94
|
-
const tag = this.inThink ? CLOSE_THINK : OPEN_THINK;
|
|
95
|
-
const at = buf.indexOf(tag);
|
|
96
|
-
if (at === -1)
|
|
97
|
-
break;
|
|
98
|
-
if (this.inThink)
|
|
99
|
-
reasoning += buf.slice(0, at);
|
|
100
|
-
else
|
|
101
|
-
answer += buf.slice(0, at);
|
|
102
|
-
buf = buf.slice(at + tag.length);
|
|
103
|
-
this.inThink = !this.inThink;
|
|
104
|
-
}
|
|
105
|
-
// Hold back only what could still become the tag we're watching for.
|
|
106
|
-
const pending = this.inThink ? CLOSE_THINK : OPEN_THINK;
|
|
107
|
-
const keep = partialTagSuffix(buf, pending);
|
|
108
|
-
this.held = keep > 0 ? buf.slice(buf.length - keep) : '';
|
|
109
|
-
const emit = keep > 0 ? buf.slice(0, buf.length - keep) : buf;
|
|
110
|
-
if (this.inThink)
|
|
111
|
-
reasoning += emit;
|
|
112
|
-
else
|
|
113
|
-
answer += emit;
|
|
114
|
-
return { reasoning, answer };
|
|
115
|
-
}
|
|
116
|
-
/** Releases the held tail; the stream ended, so it was never a tag. */
|
|
117
|
-
flush() {
|
|
118
|
-
const rest = this.held;
|
|
119
|
-
this.held = '';
|
|
120
|
-
return this.inThink
|
|
121
|
-
? { reasoning: rest, answer: '' }
|
|
122
|
-
: { reasoning: '', answer: rest };
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
/** Yields parsed `data:` payloads from an SSE response body. */
|
|
126
|
-
async function* sseChunks(res) {
|
|
127
|
-
const reader = res.body?.getReader?.();
|
|
128
|
-
if (!reader)
|
|
129
|
-
return;
|
|
130
|
-
const decoder = new TextDecoder();
|
|
131
|
-
let buf = '';
|
|
132
|
-
for (;;) {
|
|
133
|
-
const { value, done } = await reader.read();
|
|
134
|
-
if (done)
|
|
135
|
-
break;
|
|
136
|
-
buf += decoder.decode(value, { stream: true });
|
|
137
|
-
let nl;
|
|
138
|
-
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
139
|
-
const line = buf.slice(0, nl).trim();
|
|
140
|
-
buf = buf.slice(nl + 1);
|
|
141
|
-
// Blank separators and ":" keep-alive comments carry no payload.
|
|
142
|
-
if (!line.startsWith('data:'))
|
|
143
|
-
continue;
|
|
144
|
-
const payload = line.slice('data:'.length).trim();
|
|
145
|
-
if (payload === '[DONE]')
|
|
146
|
-
return;
|
|
147
|
-
try {
|
|
148
|
-
yield JSON.parse(payload);
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
/* a chunk that isn't valid JSON is not worth killing the turn for */
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
64
|
+
export function chatUsageMetadata(usage) {
|
|
65
|
+
return {
|
|
66
|
+
promptTokenCount: usage.inputTokens,
|
|
67
|
+
candidatesTokenCount: usage.outputTokens,
|
|
68
|
+
...(usage.thinkingTokens !== undefined ? { thoughtsTokenCount: usage.thinkingTokens } : {}),
|
|
69
|
+
totalTokenCount: usage.inputTokens + usage.outputTokens,
|
|
70
|
+
};
|
|
155
71
|
}
|
|
156
72
|
// ── OpenAiCompatibleLlm ──────────────────────────────────────────────────────
|
|
157
|
-
export class OpenAiCompatibleLlm extends
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
/** Model id as the wire expects it (e.g. strip the "ollama/" namespace). */
|
|
161
|
-
wireModelName() {
|
|
162
|
-
return this.model;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* How the request reaches the provider: 'direct' (the provider's own
|
|
166
|
-
* endpoint) or 'gateway:<id>' (lib/models/gatewayLlm.ts). Recorded on the
|
|
167
|
-
* llm.request span as llm.transport so the ledger can tell a native call
|
|
168
|
-
* from a proxied one while llm.provider keeps the upstream attribution.
|
|
169
|
-
*/
|
|
170
|
-
transport() {
|
|
171
|
-
return 'direct';
|
|
172
|
-
}
|
|
173
|
-
/** Provider-specific request body fields (merged last). */
|
|
174
|
-
/** Whether the endpoint accepts response_format json_schema (strict). */
|
|
175
|
-
supportsJsonSchemaFormat() {
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Whether the endpoint honors stream_options.include_usage. Without it an
|
|
180
|
-
* SSE turn reports no token counts at all; Ollama and xAI both support it.
|
|
181
|
-
*/
|
|
182
|
-
supportsStreamUsage() {
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
extraBodyFields(_llmRequest) {
|
|
186
|
-
return {};
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Body fields enabling the provider's NATIVE web search, or null when the
|
|
190
|
-
* provider has none (the base then omits the tool and warns once).
|
|
191
|
-
*/
|
|
192
|
-
webSearchBodyFields() {
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Extracts reasoning from the response message. Default: "<think>" blocks
|
|
197
|
-
* in content plus the reasoning_content / reasoning fields emitted by
|
|
198
|
-
* OpenAI-compatible reasoning models.
|
|
199
|
-
*/
|
|
200
|
-
extractReasoning(message) {
|
|
201
|
-
const { reasoning: thinkReasoning, answer } = splitThinkBlocks(message.content ?? '');
|
|
202
|
-
const fieldReasoning = message.reasoning_content ?? message.reasoning ?? '';
|
|
203
|
-
return {
|
|
204
|
-
reasoning: [fieldReasoning, thinkReasoning].filter(Boolean).join('\n\n'),
|
|
205
|
-
answer,
|
|
206
|
-
};
|
|
73
|
+
export class OpenAiCompatibleLlm extends AdkShim {
|
|
74
|
+
constructor(adapter, options = {}) {
|
|
75
|
+
super(adapter, options);
|
|
207
76
|
}
|
|
208
|
-
/**
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return undefined;
|
|
212
|
-
}
|
|
213
|
-
/** Error response for a non-2xx HTTP status. */
|
|
214
|
-
httpError(status, detail) {
|
|
215
|
-
return {
|
|
216
|
-
errorCode: `${this.providerId().toUpperCase()}_HTTP_ERROR`,
|
|
217
|
-
errorMessage: `${this.providerId()} returned ${status}: ${detail.slice(0, 4000)}`,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Whether a turn that ended with no answer after thinking (noAnswerError)
|
|
222
|
-
* is sent once more with reasoning_effort "none". Default: never. An
|
|
223
|
-
* adapter for local reasoning models turns it on (OllamaLlm).
|
|
224
|
-
*/
|
|
225
|
-
retriesWithoutThinking() {
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Error response for a turn that produced reasoning or ran out of tokens
|
|
230
|
-
* but no reply and no tool call. `truncated` is true when the provider
|
|
231
|
-
* reported finish_reason "length" (the token budget or context window
|
|
232
|
-
* filled up); false when it stopped after thinking with nothing to say.
|
|
233
|
-
*/
|
|
234
|
-
noAnswerError(truncated) {
|
|
235
|
-
const id = this.providerId();
|
|
236
|
-
return truncated
|
|
237
|
-
? {
|
|
238
|
-
errorCode: `${id.toUpperCase()}_MAX_TOKENS`,
|
|
239
|
-
errorMessage: `${this.model} ran out of tokens before it wrote a reply ` +
|
|
240
|
-
'(finish_reason "length"). Raise max_tokens, or lower ' +
|
|
241
|
-
'generateContentConfig.reasoningEffort so less goes to thinking.',
|
|
242
|
-
}
|
|
243
|
-
: {
|
|
244
|
-
errorCode: `${id.toUpperCase()}_EMPTY_RESPONSE`,
|
|
245
|
-
errorMessage: `${this.model} finished thinking but returned no reply.`,
|
|
246
|
-
};
|
|
77
|
+
/** Provider id for telemetry: the adapter's (a gateway reports the upstream provider). */
|
|
78
|
+
providerId() {
|
|
79
|
+
return this.adapter.provider;
|
|
247
80
|
}
|
|
248
|
-
/**
|
|
249
|
-
|
|
250
|
-
return
|
|
251
|
-
errorCode: `${this.providerId().toUpperCase()}_UNREACHABLE`,
|
|
252
|
-
errorMessage: `Could not reach ${this.endpointUrl()} (${message}).`,
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
// ── Generation ─────────────────────────────────────────────────────────────
|
|
256
|
-
async *generateContentAsync(llmRequest, stream = false) {
|
|
257
|
-
yield* traceLlmGeneration({ provider: this.providerId(), model: this.model, llmRequest }, this.generateInner(llmRequest, stream));
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* One request, retried once without thinking when the adapter opts in and
|
|
261
|
-
* the model thought its way to no answer (ADR 0027): a local reasoning
|
|
262
|
-
* model can spend its whole context window on the scratchpad. The first
|
|
263
|
-
* attempt's error is held back, never yielded; what it spent is added to
|
|
264
|
-
* the retry's usage, so budgets and the ledger still count it. An agent
|
|
265
|
-
* that already runs with reasoningEffort "none" is not retried.
|
|
266
|
-
*/
|
|
267
|
-
async *generateInner(llmRequest, stream) {
|
|
268
|
-
const cfg = llmRequest.config ?? {};
|
|
269
|
-
const mayRetry = this.retriesWithoutThinking() && cfg.reasoningEffort !== 'none';
|
|
270
|
-
let held;
|
|
271
|
-
for await (const r of this.attempt(llmRequest, stream)) {
|
|
272
|
-
if (mayRetry && NO_ANSWER.has(r)) {
|
|
273
|
-
held = r;
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
yield r;
|
|
277
|
-
}
|
|
278
|
-
if (!held)
|
|
279
|
-
return;
|
|
280
|
-
setLlmSpanAttribute('llm.retry_without_thinking', held.errorCode ?? true);
|
|
281
|
-
if (!this.retryWarned) {
|
|
282
|
-
this.retryWarned = true;
|
|
283
|
-
console.warn(`⚠ ${this.model} thought without answering (${held.errorCode}); retrying once with thinking off.`);
|
|
284
|
-
}
|
|
285
|
-
let carried = held.usageMetadata;
|
|
286
|
-
for await (const r of this.attempt(llmRequest, stream, 'none')) {
|
|
287
|
-
if (carried && !r.partial && (r.turnComplete || r.errorCode)) {
|
|
288
|
-
yield { ...r, usageMetadata: addUsage(carried, r.usageMetadata) };
|
|
289
|
-
carried = undefined;
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
yield r;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
async *attempt(llmRequest, stream, reasoningEffort) {
|
|
296
|
-
const missing = this.missingRequirement();
|
|
297
|
-
if (missing) {
|
|
298
|
-
yield missing;
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
setLlmSpanAttribute('llm.transport', this.transport());
|
|
302
|
-
const messages = this.buildMessages(llmRequest);
|
|
303
|
-
const openAiTools = this.buildTools(llmRequest);
|
|
304
|
-
const cfg = llmRequest.config ?? {};
|
|
305
|
-
const body = {
|
|
306
|
-
model: this.wireModelName(),
|
|
307
|
-
messages,
|
|
308
|
-
stream,
|
|
309
|
-
// SSE reports token usage only if asked, in a final choices-less chunk.
|
|
310
|
-
...(stream && this.supportsStreamUsage()
|
|
311
|
-
? { stream_options: { include_usage: true } }
|
|
312
|
-
: {}),
|
|
313
|
-
...(cfg.temperature !== undefined ? { temperature: cfg.temperature } : {}),
|
|
314
|
-
...(cfg.topP !== undefined ? { top_p: cfg.topP } : {}),
|
|
315
|
-
...(cfg.maxOutputTokens !== undefined
|
|
316
|
-
? { max_tokens: cfg.maxOutputTokens }
|
|
317
|
-
: {}),
|
|
318
|
-
// Reasoning budget. Qwen3-family models think before answering, and on
|
|
319
|
-
// a constraint-dense prompt they can spend the whole budget doing it
|
|
320
|
-
// and return an empty turn. Ollama's OpenAI-compatible endpoint honors
|
|
321
|
-
// `reasoning_effort` and IGNORES the native `think` field, so this is
|
|
322
|
-
// the only lever on this path. Opt-in per agent; omitted = unchanged.
|
|
323
|
-
...((reasoningEffort ?? cfg.reasoningEffort) !== undefined
|
|
324
|
-
? { reasoning_effort: reasoningEffort ?? cfg.reasoningEffort }
|
|
325
|
-
: {}),
|
|
326
|
-
...(openAiTools.length > 0 ? { tools: openAiTools } : {}),
|
|
327
|
-
// Structured output: an ADK outputSchema becomes a strict json_schema
|
|
328
|
-
// response_format where the endpoint supports it (xAI does; Ollama's
|
|
329
|
-
// OpenAI-compatible endpoint does not, see supportsJsonSchemaFormat),
|
|
330
|
-
// else plain JSON mode.
|
|
331
|
-
...(cfg.responseSchema && this.supportsJsonSchemaFormat()
|
|
332
|
-
? { response_format: { type: 'json_schema', json_schema: { name: 'response', strict: true, schema: toStrictJsonSchema(cfg.responseSchema) } } }
|
|
333
|
-
: cfg.responseMimeType === 'application/json' || cfg.responseSchema
|
|
334
|
-
? { response_format: { type: 'json_object' } }
|
|
335
|
-
: {}),
|
|
336
|
-
...this.extraBodyFields(llmRequest),
|
|
337
|
-
};
|
|
338
|
-
// web_search: enable the provider's native search, or omit with a warning.
|
|
339
|
-
if (wantsWebSearch(llmRequest)) {
|
|
340
|
-
const searchFields = this.webSearchBodyFields();
|
|
341
|
-
if (searchFields) {
|
|
342
|
-
Object.assign(body, searchFields);
|
|
343
|
-
setLlmSpanAttribute('llm.web_search.native', true);
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
setLlmSpanAttribute('llm.web_search.omitted', true);
|
|
347
|
-
setLlmSpanAttribute('llm.capability.dropped', 'web_search');
|
|
348
|
-
if (!this.webSearchWarned) {
|
|
349
|
-
this.webSearchWarned = true;
|
|
350
|
-
const via = this.transport().startsWith('gateway')
|
|
351
|
-
? ` through ${this.transport()} (a gateway cannot enable upstream native search)`
|
|
352
|
-
: '';
|
|
353
|
-
console.warn(`⚠ web_search requested but ${this.model}${via} has no native web search — ` +
|
|
354
|
-
'tool omitted (the agent runs without search).');
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
try {
|
|
359
|
-
// Transient failures (429/5xx, connection resets) are retried here, on
|
|
360
|
-
// the REQUEST only — the body is read after, so a stream that dies
|
|
361
|
-
// half-way is reported, never replayed (lib/models/retry.ts).
|
|
362
|
-
const signal = currentTurnSignal();
|
|
363
|
-
const { response: res } = await fetchWithRetry(this.endpointUrl(), {
|
|
364
|
-
method: 'POST',
|
|
365
|
-
headers: { 'Content-Type': 'application/json', ...this.headers() },
|
|
366
|
-
body: JSON.stringify(body),
|
|
367
|
-
signal,
|
|
368
|
-
}, {
|
|
369
|
-
signal,
|
|
370
|
-
onRetry: ({ retries }) => setLlmSpanAttribute('llm.retries', retries),
|
|
371
|
-
});
|
|
372
|
-
if (!res.ok) {
|
|
373
|
-
const detail = await res.text().catch(() => '');
|
|
374
|
-
yield withHttpStatus(this.httpError(res.status, detail), res.status);
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
if (stream) {
|
|
378
|
-
yield* this.streamResponse(res);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
const data = await res.json();
|
|
382
|
-
const choice = data.choices?.[0] ?? {};
|
|
383
|
-
const message = choice.message ?? {};
|
|
384
|
-
const usageMetadata = mapUsage(data.usage);
|
|
385
|
-
const { reasoning, answer } = this.extractReasoning(message);
|
|
386
|
-
if (reasoning) {
|
|
387
|
-
// Scratchpad, not reply: shown by printers, kept out of history.
|
|
388
|
-
yield {
|
|
389
|
-
content: {
|
|
390
|
-
role: 'model',
|
|
391
|
-
parts: [{ text: reasoning, thought: true }],
|
|
392
|
-
},
|
|
393
|
-
partial: true,
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
const parts = [];
|
|
397
|
-
if (answer)
|
|
398
|
-
parts.push({ text: answer });
|
|
399
|
-
for (const call of message.tool_calls ?? []) {
|
|
400
|
-
let args = {};
|
|
401
|
-
try {
|
|
402
|
-
args = JSON.parse(call.function?.arguments ?? '{}');
|
|
403
|
-
}
|
|
404
|
-
catch {
|
|
405
|
-
args = { raw: call.function?.arguments };
|
|
406
|
-
}
|
|
407
|
-
parts.push({
|
|
408
|
-
functionCall: {
|
|
409
|
-
name: call.function?.name,
|
|
410
|
-
args,
|
|
411
|
-
id: call.id,
|
|
412
|
-
},
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
yield this.finalResponse(parts, choice.finish_reason, Boolean(reasoning), usageMetadata);
|
|
416
|
-
}
|
|
417
|
-
catch (err) {
|
|
418
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
419
|
-
yield this.unreachable(msg);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* SSE path: display-only partials as the tokens land, then ONE final
|
|
424
|
-
* non-partial response carrying the complete text, tool calls and usage.
|
|
425
|
-
*
|
|
426
|
-
* WHY the final repeats text already streamed: ADK's runner persists only
|
|
427
|
-
* non-partial events (`if (!event.partial) appendEvent(...)`), so a final
|
|
428
|
-
* without text would show the reply on screen and lose it from session
|
|
429
|
-
* history. Printers therefore skip text on a turnComplete event whose
|
|
430
|
-
* partials they already rendered — see scripts/syndicate_chat.ts.
|
|
431
|
-
*
|
|
432
|
-
* Thinking is deliberately NOT carried on the final: the scratchpad is
|
|
433
|
-
* display-only and stays out of history, which is what keeps it from
|
|
434
|
-
* being replayed to the model on the next turn.
|
|
435
|
-
*/
|
|
436
|
-
async *streamResponse(res) {
|
|
437
|
-
const splitter = new ThinkStreamSplitter();
|
|
438
|
-
const toolCalls = new Map();
|
|
439
|
-
let answer = '';
|
|
440
|
-
let answerStarted = false;
|
|
441
|
-
let sawReasoning = false;
|
|
442
|
-
let finishReason;
|
|
443
|
-
let usage;
|
|
444
|
-
const partial = (text, thought) => {
|
|
445
|
-
if (thought)
|
|
446
|
-
sawReasoning = true;
|
|
447
|
-
return {
|
|
448
|
-
content: {
|
|
449
|
-
role: 'model',
|
|
450
|
-
parts: [thought ? { text, thought: true } : { text }],
|
|
451
|
-
},
|
|
452
|
-
partial: true,
|
|
453
|
-
};
|
|
454
|
-
};
|
|
455
|
-
// The scratchpad is usually trailed by blank lines; the reply must not
|
|
456
|
-
// open with them, but only the FIRST answer text may be trimmed.
|
|
457
|
-
const openAnswer = (text) => answerStarted ? text : text.trimStart();
|
|
458
|
-
for await (const chunk of sseChunks(res)) {
|
|
459
|
-
if (chunk.usage)
|
|
460
|
-
usage = chunk.usage;
|
|
461
|
-
const finish = chunk.choices?.[0]?.finish_reason;
|
|
462
|
-
if (finish)
|
|
463
|
-
finishReason = finish;
|
|
464
|
-
const delta = chunk.choices?.[0]?.delta;
|
|
465
|
-
if (!delta)
|
|
466
|
-
continue;
|
|
467
|
-
// Reasoning as a discrete field (Ollama `reasoning`, others
|
|
468
|
-
// `reasoning_content`) — already separated, no tag parsing needed.
|
|
469
|
-
const fieldReasoning = delta.reasoning ?? delta.reasoning_content ?? '';
|
|
470
|
-
if (fieldReasoning)
|
|
471
|
-
yield partial(fieldReasoning, true);
|
|
472
|
-
if (typeof delta.content === 'string' && delta.content) {
|
|
473
|
-
const split = splitter.push(delta.content);
|
|
474
|
-
if (split.reasoning)
|
|
475
|
-
yield partial(split.reasoning, true);
|
|
476
|
-
if (split.answer) {
|
|
477
|
-
const text = openAnswer(split.answer);
|
|
478
|
-
if (text) {
|
|
479
|
-
answerStarted = true;
|
|
480
|
-
answer += text;
|
|
481
|
-
yield partial(text, false);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
// Tool arguments arrive as fragments keyed by index, not whole.
|
|
486
|
-
for (const call of delta.tool_calls ?? []) {
|
|
487
|
-
const index = call.index ?? 0;
|
|
488
|
-
const acc = toolCalls.get(index) ?? { args: '' };
|
|
489
|
-
if (call.id)
|
|
490
|
-
acc.id = call.id;
|
|
491
|
-
if (call.function?.name)
|
|
492
|
-
acc.name = call.function.name;
|
|
493
|
-
if (call.function?.arguments)
|
|
494
|
-
acc.args += call.function.arguments;
|
|
495
|
-
toolCalls.set(index, acc);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
const tail = splitter.flush();
|
|
499
|
-
if (tail.reasoning)
|
|
500
|
-
yield partial(tail.reasoning, true);
|
|
501
|
-
if (tail.answer) {
|
|
502
|
-
const text = openAnswer(tail.answer);
|
|
503
|
-
if (text) {
|
|
504
|
-
answer += text;
|
|
505
|
-
yield partial(text, false);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
const parts = [];
|
|
509
|
-
if (answer)
|
|
510
|
-
parts.push({ text: answer });
|
|
511
|
-
for (const [, acc] of [...toolCalls.entries()].sort((a, b) => a[0] - b[0])) {
|
|
512
|
-
let args = {};
|
|
513
|
-
try {
|
|
514
|
-
args = JSON.parse(acc.args || '{}');
|
|
515
|
-
}
|
|
516
|
-
catch {
|
|
517
|
-
args = { raw: acc.args };
|
|
518
|
-
}
|
|
519
|
-
parts.push({ functionCall: { name: acc.name, args, id: acc.id } });
|
|
520
|
-
}
|
|
521
|
-
yield this.finalResponse(parts, finishReason, sawReasoning, mapUsage(usage));
|
|
522
|
-
}
|
|
523
|
-
/**
|
|
524
|
-
* The one non-partial response that closes a turn, on both paths.
|
|
525
|
-
*
|
|
526
|
-
* WHY an empty reply becomes an error: ADK drops a final response with no
|
|
527
|
-
* parts and no errorCode (llm_agent: "empty content → return"), so the
|
|
528
|
-
* last event left is the thought partial — ADK logs "The last event is
|
|
529
|
-
* partial, which is not expected." and the turn ends with empty text and
|
|
530
|
-
* no reason. That is exactly what a thinking model does when its
|
|
531
|
-
* scratchpad fills the budget (finish_reason "length"): the answer never
|
|
532
|
-
* starts. Naming it lets the caller see why, and still carries the usage
|
|
533
|
-
* so the tokens spent thinking are counted.
|
|
534
|
-
*
|
|
535
|
-
* A reply that was cut short but did start keeps its text and is marked
|
|
536
|
-
* finishReason MAX_TOKENS.
|
|
537
|
-
*/
|
|
538
|
-
finalResponse(parts, finishReason, sawReasoning, usageMetadata) {
|
|
539
|
-
if (finishReason)
|
|
540
|
-
setLlmSpanAttribute('llm.finish_reason', finishReason);
|
|
541
|
-
const truncated = finishReason === 'length';
|
|
542
|
-
const usage = usageMetadata ? { usageMetadata } : {};
|
|
543
|
-
if (parts.length === 0 && (truncated || sawReasoning)) {
|
|
544
|
-
const noAnswer = { ...this.noAnswerError(truncated), turnComplete: true, ...usage };
|
|
545
|
-
NO_ANSWER.add(noAnswer);
|
|
546
|
-
return noAnswer;
|
|
547
|
-
}
|
|
548
|
-
return {
|
|
549
|
-
content: { role: 'model', parts },
|
|
550
|
-
turnComplete: true,
|
|
551
|
-
...(truncated ? { finishReason: FinishReason.MAX_TOKENS } : {}),
|
|
552
|
-
...usage,
|
|
553
|
-
};
|
|
81
|
+
/** How the request reaches the provider: 'direct' or 'gateway:<id>' (llm.transport). */
|
|
82
|
+
transport() {
|
|
83
|
+
return this.adapter.transport();
|
|
554
84
|
}
|
|
555
|
-
|
|
556
|
-
/** ADK Contents (Google GenAI format) → OpenAI-compatible chat messages. */
|
|
85
|
+
/** The chat messages the adapter sends for this LlmRequest. */
|
|
557
86
|
buildMessages(llmRequest) {
|
|
558
|
-
|
|
559
|
-
const messages = [];
|
|
560
|
-
for (const content of llmRequest.contents) {
|
|
561
|
-
// ADK injects the system instruction as a 'system' role content.
|
|
562
|
-
if (content.role === 'system') {
|
|
563
|
-
const text = content.parts
|
|
564
|
-
?.filter((p) => p.text)
|
|
565
|
-
.map((p) => p.text)
|
|
566
|
-
.join('\n');
|
|
567
|
-
if (text)
|
|
568
|
-
systemParts.push(text);
|
|
569
|
-
continue;
|
|
570
|
-
}
|
|
571
|
-
const isModel = content.role === 'model';
|
|
572
|
-
const textChunks = [];
|
|
573
|
-
const imageParts = [];
|
|
574
|
-
const toolCalls = [];
|
|
575
|
-
for (const part of content.parts ?? []) {
|
|
576
|
-
const p = part;
|
|
577
|
-
if (p.thought) {
|
|
578
|
-
// Prior-turn scratchpad is display-only; never replay it.
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
if (p.text) {
|
|
582
|
-
textChunks.push(p.text);
|
|
583
|
-
}
|
|
584
|
-
else if (p.inlineData?.data) {
|
|
585
|
-
// ADK inline image → OpenAI image_url data URI (vision models)
|
|
586
|
-
const mime = p.inlineData.mimeType ?? 'image/png';
|
|
587
|
-
imageParts.push({
|
|
588
|
-
type: 'image_url',
|
|
589
|
-
image_url: { url: `data:${mime};base64,${p.inlineData.data}` },
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
else if (p.functionCall) {
|
|
593
|
-
// ADK function_call → OpenAI assistant tool_calls entry
|
|
594
|
-
toolCalls.push({
|
|
595
|
-
id: p.functionCall.id ?? `call_${toolCalls.length}`,
|
|
596
|
-
type: 'function',
|
|
597
|
-
function: {
|
|
598
|
-
name: p.functionCall.name,
|
|
599
|
-
arguments: JSON.stringify(p.functionCall.args ?? {}),
|
|
600
|
-
},
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
else if (p.functionResponse) {
|
|
604
|
-
// ADK function_response → OpenAI 'tool' role message
|
|
605
|
-
messages.push({
|
|
606
|
-
role: 'tool',
|
|
607
|
-
tool_call_id: p.functionResponse.id ?? '',
|
|
608
|
-
content: JSON.stringify(p.functionResponse.response ?? {}),
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
const text = textChunks.join('\n');
|
|
613
|
-
if (isModel) {
|
|
614
|
-
if (text || toolCalls.length > 0) {
|
|
615
|
-
messages.push({
|
|
616
|
-
role: 'assistant',
|
|
617
|
-
content: text || null,
|
|
618
|
-
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
else if (text || imageParts.length > 0) {
|
|
623
|
-
messages.push({
|
|
624
|
-
role: 'user',
|
|
625
|
-
content: imageParts.length > 0
|
|
626
|
-
? [...(text ? [{ type: 'text', text }] : []), ...imageParts]
|
|
627
|
-
: text,
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
// Also pull system instruction from generateContentConfig if present
|
|
632
|
-
const configSystem = llmRequest.config?.systemInstruction;
|
|
633
|
-
if (configSystem) {
|
|
634
|
-
const text = typeof configSystem === 'string'
|
|
635
|
-
? configSystem
|
|
636
|
-
: configSystem.parts?.map((p) => p.text).join('\n') ?? '';
|
|
637
|
-
if (text)
|
|
638
|
-
systemParts.unshift(text);
|
|
639
|
-
}
|
|
640
|
-
if (systemParts.length > 0) {
|
|
641
|
-
messages.unshift({ role: 'system', content: systemParts.join('\n\n') });
|
|
642
|
-
}
|
|
643
|
-
return messages;
|
|
87
|
+
return this.adapter.messagesFor(this.toModelRequest(llmRequest, { model: this.model }));
|
|
644
88
|
}
|
|
645
|
-
/**
|
|
89
|
+
/** The function tools the adapter sends for this LlmRequest (lowercase schemas). */
|
|
646
90
|
buildTools(llmRequest) {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
async connect(_llmRequest) {
|
|
670
|
-
throw new Error(`${this.constructor.name} does not support live bidirectional connections. ` +
|
|
671
|
-
'Use a Gemini model for live/streaming sessions.');
|
|
91
|
+
return this.adapter.toolsFor(this.toModelRequest(llmRequest, { model: this.model }));
|
|
92
|
+
}
|
|
93
|
+
/** The genai mapping's request, with the older spelling the contract leaves out. */
|
|
94
|
+
toModelRequest(llmRequest, options) {
|
|
95
|
+
const request = super.toModelRequest(llmRequest, options);
|
|
96
|
+
const olderSpelling = olderSpellingOf(llmRequest.config);
|
|
97
|
+
return olderSpelling ? { ...request, olderSpelling } : request;
|
|
98
|
+
}
|
|
99
|
+
/** The genai mapping's response, in the shape these classes have always yielded (see the header). */
|
|
100
|
+
toLlmResponse(response) {
|
|
101
|
+
const mapped = super.toLlmResponse(response);
|
|
102
|
+
if (response.partial)
|
|
103
|
+
return mapped;
|
|
104
|
+
const { finishReason: _finish, usageMetadata: _usage, ...rest } = mapped;
|
|
105
|
+
const error = response.error;
|
|
106
|
+
return {
|
|
107
|
+
...(error ? {} : { content: { role: 'model', parts: [] } }),
|
|
108
|
+
...rest,
|
|
109
|
+
...(!error && response.finishReason === 'max_tokens' ? { finishReason: FinishReason.MAX_TOKENS } : {}),
|
|
110
|
+
...(response.usage ? { usageMetadata: chatUsageMetadata(response.usage) } : {}),
|
|
111
|
+
...(error?.status !== undefined ? { status: error.status, retryable: error.retryable } : {}),
|
|
112
|
+
};
|
|
672
113
|
}
|
|
673
114
|
}
|
|
115
|
+
// ── Kept for callers of the ADK-path helpers ─────────────────────────────────
|
|
674
116
|
/**
|
|
675
117
|
* Stamps an HTTP error response with its numeric status and whether that
|
|
676
|
-
* status is transient,
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
* errorCode and wording and still gets the fields.
|
|
118
|
+
* status is transient, and the same verdict in customMetadata
|
|
119
|
+
* ('error.retryable', 'error.status'), where FallbackLlm reads it
|
|
120
|
+
* (lib/models/errorResponse.ts). The chat-completions shims take these
|
|
121
|
+
* fields from their adapter's error final instead (toLlmResponse).
|
|
681
122
|
*/
|
|
682
123
|
export function withHttpStatus(resp, status) {
|
|
683
124
|
setLlmSpanAttribute('llm.http_status', status);
|
|
684
|
-
return { ...resp, status, retryable: isRetryableStatus(status) };
|
|
125
|
+
return { ...withRetryVerdict(resp, statusDecision(status)), status, retryable: isRetryableStatus(status) };
|
|
685
126
|
}
|
|
686
|
-
/**
|
|
687
|
-
/** Responses built by noAnswerError: the ones a retry without thinking may replace. */
|
|
688
|
-
const NO_ANSWER = new WeakSet();
|
|
689
|
-
/** Two usage records summed, field by field. */
|
|
127
|
+
/** Two genai usage records summed, field by field. */
|
|
690
128
|
export function addUsage(a, b) {
|
|
691
129
|
if (!a)
|
|
692
130
|
return b;
|
|
@@ -699,20 +137,20 @@ export function addUsage(a, b) {
|
|
|
699
137
|
}
|
|
700
138
|
return out;
|
|
701
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* OpenAI-style usage → GenAI usageMetadata in the ADK-path meaning
|
|
142
|
+
* (completion_tokens, reasoning included, as candidatesTokenCount), or
|
|
143
|
+
* undefined when absent. The contract's meaning is chatUsage in
|
|
144
|
+
* lib/models/chatCompletionsAdapter.ts.
|
|
145
|
+
*/
|
|
702
146
|
export function mapUsage(usage) {
|
|
703
147
|
if (!usage || typeof usage !== 'object')
|
|
704
148
|
return undefined;
|
|
705
149
|
const reasoning = usage.completion_tokens_details?.reasoning_tokens;
|
|
706
150
|
return {
|
|
707
|
-
...(usage.prompt_tokens !== undefined
|
|
708
|
-
|
|
709
|
-
: {}),
|
|
710
|
-
...(usage.completion_tokens !== undefined
|
|
711
|
-
? { candidatesTokenCount: usage.completion_tokens }
|
|
712
|
-
: {}),
|
|
151
|
+
...(usage.prompt_tokens !== undefined ? { promptTokenCount: usage.prompt_tokens } : {}),
|
|
152
|
+
...(usage.completion_tokens !== undefined ? { candidatesTokenCount: usage.completion_tokens } : {}),
|
|
713
153
|
...(reasoning !== undefined ? { thoughtsTokenCount: reasoning } : {}),
|
|
714
|
-
...(usage.total_tokens !== undefined
|
|
715
|
-
? { totalTokenCount: usage.total_tokens }
|
|
716
|
-
: {}),
|
|
154
|
+
...(usage.total_tokens !== undefined ? { totalTokenCount: usage.total_tokens } : {}),
|
|
717
155
|
};
|
|
718
156
|
}
|