melchizedek-agents 0.9.6 → 0.16.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 +272 -0
- package/AGENT_SETUP.md +39 -23
- package/CHANGELOG.md +623 -0
- package/DOCUMENTATION.md +429 -109
- package/QUICKSTART.md +92 -18
- package/README.md +45 -5
- package/config/agents/README.md +46 -1
- package/config/agents/examples/ares.yaml +117 -40
- package/config/agents/examples/assistant.yaml +159 -0
- package/config/agents/examples/augustin.yaml +154 -51
- package/config/agents/examples/cartographers.yaml +91 -23
- package/config/agents/examples/claude.yaml +45 -6
- package/config/agents/examples/council.yaml +23 -8
- package/config/agents/examples/critic.yaml +57 -21
- package/config/agents/examples/delegation.yaml +76 -9
- package/config/agents/examples/hierarchical.yaml +55 -8
- package/config/agents/examples/image_production.yaml +54 -16
- package/config/agents/examples/librarian.yaml +70 -14
- package/config/agents/examples/model_zoo.yaml +105 -29
- package/config/agents/examples/patient_advocate.yaml +102 -34
- package/config/agents/examples/research.yaml +250 -0
- package/config/agents/examples/scribe.yaml +242 -0
- package/config/agents/examples/scriptorium.yaml +87 -24
- package/config/agents/examples/style_council.yaml +31 -9
- package/config/agents/examples/syndicate.yaml +29 -10
- package/config/agents/examples/tutor.yaml +10 -3
- package/config/agents/syndicateSchema.yaml +44 -9
- package/config/agents/templates/README.md +58 -0
- package/config/agents/templates/account_memory.yaml +153 -0
- package/config/agents/templates/case_desk.yaml +188 -0
- package/config/agents/templates/conversational.yaml +110 -0
- package/config/agents/templates/draft_review.yaml +130 -0
- package/config/agents/templates/intake_extractor.yaml +152 -0
- package/config/agents/templates/research_brief.yaml +169 -0
- package/config/agents/templates/research_desk.yaml +152 -0
- package/config/agents/templates/review_panel.yaml +188 -0
- package/config/agents/templates/support_triage.yaml +273 -0
- package/config/agents/templates/systems_operator.yaml +157 -0
- package/db/hardening.sql +202 -0
- package/db/memory_v2.sql +77 -0
- package/db/migrations/0001_base.sql +167 -0
- package/db/migrations/0002_erase_scope.sql +172 -0
- package/db/migrations/0003_postgres_storage.sql +58 -0
- package/db/migrations/0004_usage.sql +94 -0
- package/db/migrations/0005_agent_registry.sql +200 -0
- package/db/migrations/0006_task_leases.sql +25 -0
- package/db/migrations/0007_memory_commit.sql +134 -0
- package/db/migrations/0008_memory_retention.sql +37 -0
- package/db/migrations/0009_task_queue.sql +51 -0
- package/db/migrations/0010_erase_complete.sql +180 -0
- package/db/telemetry.sql +401 -0
- package/demo/README.md +77 -0
- package/demo/a2a_demo.mjs +95 -0
- package/dist/lib/a2a/app.d.ts +253 -0
- package/dist/lib/a2a/app.js +751 -0
- package/dist/lib/a2a/executor.d.ts +167 -0
- package/dist/lib/a2a/executor.js +548 -0
- package/dist/lib/a2a/identity.d.ts +122 -0
- package/dist/lib/a2a/identity.js +254 -0
- package/dist/lib/a2a/policy.d.ts +110 -0
- package/dist/lib/a2a/policy.js +178 -0
- package/dist/lib/a2a/redisLimits.d.ts +26 -0
- package/dist/lib/a2a/redisLimits.js +31 -0
- package/dist/lib/a2a/remoteAgent.d.ts +91 -0
- package/dist/lib/a2a/remoteAgent.js +268 -0
- package/dist/lib/a2a/turnLock.d.ts +28 -0
- package/dist/lib/a2a/turnLock.js +76 -0
- package/dist/lib/compile.d.ts +11 -1
- package/dist/lib/compile.js +109 -8
- package/dist/lib/config.d.ts +7 -7
- package/dist/lib/config.js +7 -7
- package/dist/lib/doctor.d.ts +145 -0
- package/dist/lib/doctor.js +517 -0
- package/dist/lib/grounding.d.ts +2 -2
- package/dist/lib/grounding.js +2 -2
- package/dist/lib/guards/index.d.ts +37 -0
- package/dist/lib/guards/index.js +66 -0
- package/dist/lib/guards/science.d.ts +117 -0
- package/dist/lib/guards/science.js +444 -0
- package/dist/lib/index.d.ts +37 -2
- package/dist/lib/index.js +35 -4
- package/dist/lib/loadEnv.d.ts +23 -11
- package/dist/lib/loadEnv.js +77 -29
- package/dist/lib/loadSyndicate.d.ts +97 -5
- package/dist/lib/loadSyndicate.js +176 -76
- package/dist/lib/memory/erase.d.ts +24 -0
- package/dist/lib/memory/erase.js +47 -0
- package/dist/lib/memory/namespace.d.ts +37 -0
- package/dist/lib/memory/namespace.js +74 -0
- package/dist/lib/memory/providers.d.ts +99 -0
- package/dist/lib/memory/providers.js +240 -0
- package/dist/lib/memory/store.d.ts +74 -0
- package/dist/lib/memory/store.js +108 -0
- package/dist/lib/memory/supabaseMemoryService.d.ts +69 -9
- package/dist/lib/memory/supabaseMemoryService.js +239 -125
- package/dist/lib/models/capabilities.d.ts +103 -0
- package/dist/lib/models/capabilities.js +291 -0
- package/dist/lib/models/claudeLlm.d.ts +10 -1
- package/dist/lib/models/claudeLlm.js +57 -27
- package/dist/lib/models/endpoints.d.ts +100 -0
- package/dist/lib/models/endpoints.js +282 -0
- package/dist/lib/models/gateway.d.ts +107 -0
- package/dist/lib/models/gateway.js +157 -0
- package/dist/lib/models/gatewayLlm.d.ts +57 -0
- package/dist/lib/models/gatewayLlm.js +105 -0
- package/dist/lib/models/gptLlm.d.ts +51 -2
- package/dist/lib/models/gptLlm.js +169 -26
- package/dist/lib/models/grokLlm.d.ts +19 -10
- package/dist/lib/models/grokLlm.js +29 -14
- package/dist/lib/models/ollamaLlm.d.ts +26 -0
- package/dist/lib/models/ollamaLlm.js +43 -1
- package/dist/lib/models/openAiCompatibleLlm.d.ts +67 -2
- package/dist/lib/models/openAiCompatibleLlm.js +200 -37
- package/dist/lib/models/providerMap.js +9 -2
- package/dist/lib/models/registry.d.ts +42 -5
- package/dist/lib/models/registry.js +140 -29
- package/dist/lib/models/retry.d.ts +146 -0
- package/dist/lib/models/retry.js +305 -0
- package/dist/lib/models/schemaNormalize.d.ts +20 -0
- package/dist/lib/models/schemaNormalize.js +43 -0
- package/dist/lib/net/addressGuard.d.ts +60 -0
- package/dist/lib/net/addressGuard.js +216 -0
- package/dist/lib/observability/embeddings.d.ts +11 -2
- package/dist/lib/observability/embeddings.js +18 -14
- package/dist/lib/observability/metrics.d.ts +36 -0
- package/dist/lib/observability/metrics.js +86 -0
- package/dist/lib/observability/redact.d.ts +29 -0
- package/dist/lib/observability/redact.js +102 -0
- package/dist/lib/observability/supabaseSpanExporter.d.ts +2 -0
- package/dist/lib/observability/supabaseSpanExporter.js +22 -6
- package/dist/lib/observability/tracer.d.ts +10 -0
- package/dist/lib/observability/tracer.js +70 -3
- package/dist/lib/persistence/supabaseProvider.d.ts +11 -0
- package/dist/lib/persistence/supabaseProvider.js +32 -6
- package/dist/lib/registry.d.ts +87 -0
- package/dist/lib/registry.js +146 -0
- package/dist/lib/runtime/approvals.d.ts +44 -0
- package/dist/lib/runtime/approvals.js +67 -0
- package/dist/lib/runtime/syndicateTurn.d.ts +233 -0
- package/dist/lib/runtime/syndicateTurn.js +597 -0
- package/dist/lib/runtime/turnControl.d.ts +90 -0
- package/dist/lib/runtime/turnControl.js +131 -0
- package/dist/lib/session/supabaseSessionService.js +1 -1
- package/dist/lib/session/transcript.d.ts +9 -1
- package/dist/lib/session/transcript.js +15 -7
- package/dist/lib/skills.d.ts +50 -0
- package/dist/lib/skills.js +138 -0
- package/dist/lib/storage/postgres/index.d.ts +98 -0
- package/dist/lib/storage/postgres/index.js +146 -0
- package/dist/lib/storage/postgres/memoryStore.d.ts +11 -0
- package/dist/lib/storage/postgres/memoryStore.js +82 -0
- package/dist/lib/storage/postgres/sessionService.d.ts +51 -0
- package/dist/lib/storage/postgres/sessionService.js +225 -0
- package/dist/lib/storage/postgres/taskQueue.d.ts +22 -0
- package/dist/lib/storage/postgres/taskQueue.js +101 -0
- package/dist/lib/storage/postgres/taskStore.d.ts +55 -0
- package/dist/lib/storage/postgres/taskStore.js +168 -0
- package/dist/lib/storage/schema.d.ts +27 -0
- package/dist/lib/storage/schema.js +50 -0
- package/dist/lib/storage/schemaVersion.d.ts +25 -0
- package/dist/lib/storage/schemaVersion.js +46 -0
- package/dist/lib/syndicateSchema.d.ts +275 -0
- package/dist/lib/syndicateSchema.js +534 -0
- package/dist/lib/toolRegistry.d.ts +15 -0
- package/dist/lib/toolRegistry.js +59 -10
- package/dist/lib/tools/collectionsSearchTool.js +1 -1
- package/dist/lib/tools/mcpServe.d.ts +45 -0
- package/dist/lib/tools/mcpServe.js +88 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
- package/dist/lib/tools/mcpToolFactory.js +41 -21
- package/dist/lib/tools/science/corrections.d.ts +42 -0
- package/dist/lib/tools/science/corrections.js +115 -0
- package/dist/lib/tools/science/europepmc.d.ts +51 -0
- package/dist/lib/tools/science/europepmc.js +127 -0
- package/dist/lib/tools/science/http.d.ts +31 -0
- package/dist/lib/tools/science/http.js +104 -0
- package/dist/lib/tools/science/openalex.d.ts +30 -0
- package/dist/lib/tools/science/openalex.js +81 -0
- package/dist/lib/tools/science/trials.d.ts +58 -0
- package/dist/lib/tools/science/trials.js +203 -0
- package/dist/lib/tools/science/types.d.ts +69 -0
- package/dist/lib/tools/science/types.js +48 -0
- package/dist/lib/tools/scienceTools.d.ts +100 -0
- package/dist/lib/tools/scienceTools.js +277 -0
- package/dist/lib/tools/taskTools.d.ts +204 -0
- package/dist/lib/tools/taskTools.js +374 -0
- package/dist/lib/tools/toolContract.d.ts +17 -4
- package/dist/lib/tools/toolContract.js +13 -5
- package/dist/lib/tools/webExtractTool.d.ts +13 -10
- package/dist/lib/tools/webExtractTool.js +23 -55
- package/dist/lib/tools/webSearchTool.d.ts +2 -1
- package/dist/lib/tools/webSearchTool.js +4 -3
- package/dist/lib/tools/wikiTools.d.ts +2 -2
- package/dist/lib/tools/wikiTools.js +12 -12
- package/dist/lib/tools/xApiSearchTool.d.ts +194 -0
- package/dist/lib/tools/xApiSearchTool.js +583 -0
- package/dist/lib/tools/xSearchTool.js +1 -1
- package/dist/lib/wiki/agentRun.js +1 -1
- package/dist/lib/wiki/builder.js +21 -6
- package/dist/lib/wiki/dive.js +2 -2
- package/dist/lib/wiki/entities.d.ts +2 -2
- package/dist/lib/wiki/entities.js +7 -7
- package/dist/lib/wiki/extract.js +5 -1
- package/dist/lib/wiki/fill.js +5 -5
- package/dist/lib/wiki/format.d.ts +7 -5
- package/dist/lib/wiki/format.js +2 -2
- package/dist/lib/wiki/graph.js +2 -2
- package/dist/lib/wiki/lint.js +2 -2
- package/dist/lib/wiki/markdown.d.ts +1 -1
- package/dist/lib/wiki/markdown.js +6 -1
- package/dist/lib/wiki/pipeline.js +2 -2
- package/dist/lib/wiki/vault.js +2 -2
- package/dist/scripts/a2a_server.d.ts +17 -11
- package/dist/scripts/a2a_server.js +405 -1028
- package/dist/scripts/assistant_worker.d.ts +33 -0
- package/dist/scripts/assistant_worker.js +193 -0
- package/dist/scripts/db.d.ts +26 -0
- package/dist/scripts/db.js +179 -0
- package/dist/scripts/doctor.d.ts +23 -0
- package/dist/scripts/doctor.js +90 -0
- package/dist/scripts/init.d.ts +43 -0
- package/dist/scripts/init.js +152 -0
- package/dist/scripts/registry.d.ts +25 -0
- package/dist/scripts/registry.js +182 -0
- package/dist/scripts/skills_install.d.ts +7 -0
- package/dist/scripts/skills_install.js +137 -0
- package/dist/scripts/syndicate_chat.js +159 -276
- package/package.json +90 -16
- package/skills/README.md +74 -0
- package/skills/briefs/README.md +16 -0
- package/skills/briefs/_shared.md +16 -0
- package/skills/briefs/melchizedek-author.md +60 -0
- package/skills/briefs/melchizedek-memory.md +47 -0
- package/skills/briefs/melchizedek-models.md +47 -0
- package/skills/briefs/melchizedek-scribe.md +37 -0
- package/skills/briefs/melchizedek-serve.md +56 -0
- package/skills/briefs/melchizedek.md +69 -0
- package/skills/briefs/skills-readme.md +33 -0
- package/skills/melchizedek/SKILL.md +159 -0
- package/skills/melchizedek-author/SKILL.md +99 -0
- package/skills/melchizedek-author/templates/minimal.yaml +60 -0
- package/skills/melchizedek-memory/SKILL.md +138 -0
- package/skills/melchizedek-models/SKILL.md +137 -0
- package/skills/melchizedek-scribe/SKILL.md +72 -0
- package/skills/melchizedek-scribe/templates/brief.md +27 -0
- package/skills/melchizedek-serve/SKILL.md +119 -0
|
@@ -71,9 +71,19 @@
|
|
|
71
71
|
* - generate_image / inspect_image call Gemini image models directly and
|
|
72
72
|
* still require a Gemini key regardless of the inference model.
|
|
73
73
|
* - thinkingConfig is Gemini-only; qwen3 thinks on its own schedule.
|
|
74
|
+
* generateContentConfig.reasoningEffort travels as reasoning_effort, and
|
|
75
|
+
* on Ollama 0.31 only "none" changes anything (it turns thinking off);
|
|
76
|
+
* "low" does not bound a qwen3.5 scratchpad, and `think: false` is
|
|
77
|
+
* ignored on this path.
|
|
78
|
+
* - Context window: 4,096 tokens unless the Modelfile (PARAMETER num_ctx)
|
|
79
|
+
* or OLLAMA_CONTEXT_LENGTH on the server says otherwise — /v1 ignores
|
|
80
|
+
* num_ctx. A thinking model that fills it, or stops after thinking, is
|
|
81
|
+
* retried once with thinking off (retriesWithoutThinking); only if that
|
|
82
|
+
* fails too does the turn end with OLLAMA_MAX_TOKENS or
|
|
83
|
+
* OLLAMA_EMPTY_RESPONSE, never an empty reply (see noAnswerError).
|
|
74
84
|
*/
|
|
75
85
|
import { LLMRegistry } from '@google/adk';
|
|
76
|
-
import { OpenAiCompatibleLlm } from
|
|
86
|
+
import { OpenAiCompatibleLlm } from './openAiCompatibleLlm.js';
|
|
77
87
|
const DEFAULT_BASE_URL = 'http://localhost:11434/v1';
|
|
78
88
|
// ── OllamaLlm ─────────────────────────────────────────────────────────────────
|
|
79
89
|
export class OllamaLlm extends OpenAiCompatibleLlm {
|
|
@@ -119,9 +129,41 @@ export class OllamaLlm extends OpenAiCompatibleLlm {
|
|
|
119
129
|
'Is it running? Start the Ollama app or run: ollama serve',
|
|
120
130
|
};
|
|
121
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Out of tokens on Ollama almost always means the CONTEXT WINDOW, not
|
|
134
|
+
* max_tokens: Ollama loads a model with a 4,096-token window unless told
|
|
135
|
+
* otherwise, the prompt and the scratchpad share it, and this /v1 path
|
|
136
|
+
* ignores num_ctx (and `options` entirely). A qwen3.5 explainer prompt can
|
|
137
|
+
* think for 3,700+ tokens and hit the ceiling before the answer starts.
|
|
138
|
+
* The two levers that work are named in the message.
|
|
139
|
+
*/
|
|
140
|
+
noAnswerError(truncated) {
|
|
141
|
+
if (!truncated)
|
|
142
|
+
return super.noAnswerError(truncated);
|
|
143
|
+
const name = this.wireModelName();
|
|
144
|
+
return {
|
|
145
|
+
errorCode: 'OLLAMA_MAX_TOKENS',
|
|
146
|
+
errorMessage: `${name} filled its context window while thinking and never wrote a reply ` +
|
|
147
|
+
'(finish_reason "length"). Ollama defaults to a 4,096-token window and its ' +
|
|
148
|
+
'OpenAI-compatible endpoint ignores num_ctx, so either set ' +
|
|
149
|
+
'generateContentConfig.reasoningEffort: "none" on the agent to skip thinking, ' +
|
|
150
|
+
`or give the model a larger window: a Modelfile with "FROM ${name}" and ` +
|
|
151
|
+
'"PARAMETER num_ctx 32768" (ollama create), or OLLAMA_CONTEXT_LENGTH=32768 ' +
|
|
152
|
+
'on the ollama serve process.',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
122
155
|
extraBodyFields(_llmRequest) {
|
|
123
156
|
return {};
|
|
124
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* A local reasoning model that thinks its way to no answer is asked once
|
|
160
|
+
* more with thinking off ("none" is the one reasoning_effort Ollama 0.31
|
|
161
|
+
* honors), so the turn answers instead of failing. Off with
|
|
162
|
+
* OLLAMA_RETRY_WITHOUT_THINKING=false.
|
|
163
|
+
*/
|
|
164
|
+
retriesWithoutThinking() {
|
|
165
|
+
return process.env.OLLAMA_RETRY_WITHOUT_THINKING?.trim().toLowerCase() !== 'false';
|
|
166
|
+
}
|
|
125
167
|
}
|
|
126
168
|
// ── Registration helper ───────────────────────────────────────────────────────
|
|
127
169
|
/**
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
* surfaced as a { text, thought: true } part in a partial response —
|
|
24
24
|
* printers display it dimmed; it never enters session history.
|
|
25
25
|
* - A per-request `llm.request` OpenTelemetry span (lib/observability).
|
|
26
|
+
* - Retries of transient failures (lib/models/retry.ts) before any byte
|
|
27
|
+
* is yielded, and HTTP errors carrying `status` + `retryable`.
|
|
26
28
|
* - web_search handling: subclasses with native search return its body
|
|
27
29
|
* fields from webSearchBodyFields(); those without return null and the
|
|
28
30
|
* base omits the tool with a one-time warning + span attribute.
|
|
@@ -61,7 +63,13 @@ type OpenAiMessage = {
|
|
|
61
63
|
tool_call_id: string;
|
|
62
64
|
content: string;
|
|
63
65
|
};
|
|
64
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Splits "<think>…</think>" scratchpad from the reply.
|
|
68
|
+
*
|
|
69
|
+
* A block the model never closed is scratchpad too: it means the model ran
|
|
70
|
+
* out of budget mid-thought, so everything after the opening tag is
|
|
71
|
+
* reasoning — never reply text with a raw "<think>" in it.
|
|
72
|
+
*/
|
|
65
73
|
export declare function splitThinkBlocks(text: string): {
|
|
66
74
|
reasoning: string;
|
|
67
75
|
answer: string;
|
|
@@ -95,6 +103,7 @@ export declare class ThinkStreamSplitter {
|
|
|
95
103
|
}
|
|
96
104
|
export declare abstract class OpenAiCompatibleLlm extends BaseLlm {
|
|
97
105
|
private webSearchWarned;
|
|
106
|
+
private retryWarned;
|
|
98
107
|
/** Provider id for telemetry, e.g. 'ollama', 'xai'. */
|
|
99
108
|
protected abstract providerId(): string;
|
|
100
109
|
/** Full chat-completions URL, e.g. "https://api.x.ai/v1/chat/completions". */
|
|
@@ -103,6 +112,13 @@ export declare abstract class OpenAiCompatibleLlm extends BaseLlm {
|
|
|
103
112
|
protected abstract headers(): Record<string, string>;
|
|
104
113
|
/** Model id as the wire expects it (e.g. strip the "ollama/" namespace). */
|
|
105
114
|
protected wireModelName(): string;
|
|
115
|
+
/**
|
|
116
|
+
* How the request reaches the provider: 'direct' (the provider's own
|
|
117
|
+
* endpoint) or 'gateway:<id>' (lib/models/gatewayLlm.ts). Recorded on the
|
|
118
|
+
* llm.request span as llm.transport so the ledger can tell a native call
|
|
119
|
+
* from a proxied one while llm.provider keeps the upstream attribution.
|
|
120
|
+
*/
|
|
121
|
+
protected transport(): string;
|
|
106
122
|
/** Provider-specific request body fields (merged last). */
|
|
107
123
|
/** Whether the endpoint accepts response_format json_schema (strict). */
|
|
108
124
|
protected supportsJsonSchemaFormat(): boolean;
|
|
@@ -135,10 +151,32 @@ export declare abstract class OpenAiCompatibleLlm extends BaseLlm {
|
|
|
135
151
|
protected missingRequirement(): LlmResponse | undefined;
|
|
136
152
|
/** Error response for a non-2xx HTTP status. */
|
|
137
153
|
protected httpError(status: number, detail: string): LlmResponse;
|
|
154
|
+
/**
|
|
155
|
+
* Whether a turn that ended with no answer after thinking (noAnswerError)
|
|
156
|
+
* is sent once more with reasoning_effort "none". Default: never. An
|
|
157
|
+
* adapter for local reasoning models turns it on (OllamaLlm).
|
|
158
|
+
*/
|
|
159
|
+
protected retriesWithoutThinking(): boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Error response for a turn that produced reasoning or ran out of tokens
|
|
162
|
+
* but no reply and no tool call. `truncated` is true when the provider
|
|
163
|
+
* reported finish_reason "length" (the token budget or context window
|
|
164
|
+
* filled up); false when it stopped after thinking with nothing to say.
|
|
165
|
+
*/
|
|
166
|
+
protected noAnswerError(truncated: boolean): LlmResponse;
|
|
138
167
|
/** Error response when the endpoint can't be reached at all. */
|
|
139
168
|
protected unreachable(message: string): LlmResponse;
|
|
140
169
|
generateContentAsync(llmRequest: LlmRequest, stream?: boolean): AsyncGenerator<LlmResponse, void>;
|
|
170
|
+
/**
|
|
171
|
+
* One request, retried once without thinking when the adapter opts in and
|
|
172
|
+
* the model thought its way to no answer (ADR 0027): a local reasoning
|
|
173
|
+
* model can spend its whole context window on the scratchpad. The first
|
|
174
|
+
* attempt's error is held back, never yielded; what it spent is added to
|
|
175
|
+
* the retry's usage, so budgets and the ledger still count it. An agent
|
|
176
|
+
* that already runs with reasoningEffort "none" is not retried.
|
|
177
|
+
*/
|
|
141
178
|
private generateInner;
|
|
179
|
+
private attempt;
|
|
142
180
|
/**
|
|
143
181
|
* SSE path: display-only partials as the tokens land, then ONE final
|
|
144
182
|
* non-partial response carrying the complete text, tool calls and usage.
|
|
@@ -154,6 +192,22 @@ export declare abstract class OpenAiCompatibleLlm extends BaseLlm {
|
|
|
154
192
|
* being replayed to the model on the next turn.
|
|
155
193
|
*/
|
|
156
194
|
private streamResponse;
|
|
195
|
+
/**
|
|
196
|
+
* The one non-partial response that closes a turn, on both paths.
|
|
197
|
+
*
|
|
198
|
+
* WHY an empty reply becomes an error: ADK drops a final response with no
|
|
199
|
+
* parts and no errorCode (llm_agent: "empty content → return"), so the
|
|
200
|
+
* last event left is the thought partial — ADK logs "The last event is
|
|
201
|
+
* partial, which is not expected." and the turn ends with empty text and
|
|
202
|
+
* no reason. That is exactly what a thinking model does when its
|
|
203
|
+
* scratchpad fills the budget (finish_reason "length"): the answer never
|
|
204
|
+
* starts. Naming it lets the caller see why, and still carries the usage
|
|
205
|
+
* so the tokens spent thinking are counted.
|
|
206
|
+
*
|
|
207
|
+
* A reply that was cut short but did start keeps its text and is marked
|
|
208
|
+
* finishReason MAX_TOKENS.
|
|
209
|
+
*/
|
|
210
|
+
private finalResponse;
|
|
157
211
|
/** ADK Contents (Google GenAI format) → OpenAI-compatible chat messages. */
|
|
158
212
|
protected buildMessages(llmRequest: LlmRequest): OpenAiMessage[];
|
|
159
213
|
/** ADK toolsDict → OpenAI function-tool definitions (lowercase schemas). */
|
|
@@ -164,7 +218,18 @@ export declare abstract class OpenAiCompatibleLlm extends BaseLlm {
|
|
|
164
218
|
*/
|
|
165
219
|
connect(_llmRequest: LlmRequest): Promise<BaseLlmConnection>;
|
|
166
220
|
}
|
|
167
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* Stamps an HTTP error response with its numeric status and whether that
|
|
223
|
+
* status is transient, so callers above (and the ledger, via the tracer's
|
|
224
|
+
* llm.payload.response) can tell a rate limit from a bad request without
|
|
225
|
+
* parsing the message. Applied in the base so a subclass's httpError
|
|
226
|
+
* override (Ollama's hint, the gateway's GATEWAY_HTTP_ERROR) keeps its
|
|
227
|
+
* errorCode and wording and still gets the fields.
|
|
228
|
+
*/
|
|
229
|
+
export declare function withHttpStatus(resp: LlmResponse, status: number): LlmResponse;
|
|
230
|
+
type Usage = NonNullable<ReturnType<typeof mapUsage>>;
|
|
231
|
+
/** Two usage records summed, field by field. */
|
|
232
|
+
export declare function addUsage(a: Usage | undefined, b: Usage | undefined): Usage | undefined;
|
|
168
233
|
export declare function mapUsage(usage: any): {
|
|
169
234
|
promptTokenCount?: number;
|
|
170
235
|
candidatesTokenCount?: number;
|
|
@@ -23,27 +23,43 @@
|
|
|
23
23
|
* surfaced as a { text, thought: true } part in a partial response —
|
|
24
24
|
* printers display it dimmed; it never enters session history.
|
|
25
25
|
* - A per-request `llm.request` OpenTelemetry span (lib/observability).
|
|
26
|
+
* - Retries of transient failures (lib/models/retry.ts) before any byte
|
|
27
|
+
* is yielded, and HTTP errors carrying `status` + `retryable`.
|
|
26
28
|
* - web_search handling: subclasses with native search return its body
|
|
27
29
|
* fields from webSearchBodyFields(); those without return null and the
|
|
28
30
|
* base omits the tool with a one-time warning + span attribute.
|
|
29
31
|
*/
|
|
30
32
|
import { BaseLlm } from '@google/adk';
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
|
|
33
|
+
import { FinishReason } from '@google/genai';
|
|
34
|
+
import { traceLlmGeneration, setLlmSpanAttribute, } from '../observability/tracer.js';
|
|
35
|
+
import { wantsWebSearch, isWebSearchSentinel, } from '../tools/webSearchTool.js';
|
|
36
|
+
import { currentTurnSignal } from '../runtime/turnControl.js';
|
|
37
|
+
import { toStrictJsonSchema, toolDeclarationFor } from './schemaNormalize.js';
|
|
38
|
+
import { fetchWithRetry, isRetryableStatus } from './retry.js';
|
|
39
|
+
const OPEN_THINK = '<think>';
|
|
40
|
+
const CLOSE_THINK = '</think>';
|
|
41
|
+
/**
|
|
42
|
+
* Splits "<think>…</think>" scratchpad from the reply.
|
|
43
|
+
*
|
|
44
|
+
* A block the model never closed is scratchpad too: it means the model ran
|
|
45
|
+
* out of budget mid-thought, so everything after the opening tag is
|
|
46
|
+
* reasoning — never reply text with a raw "<think>" in it.
|
|
47
|
+
*/
|
|
35
48
|
export function splitThinkBlocks(text) {
|
|
36
49
|
const blocks = [];
|
|
37
|
-
|
|
38
|
-
.replace(/<think>([\s\S]*?)<\/think>/g, (_, inner) => {
|
|
50
|
+
let answer = text.replace(/<think>([\s\S]*?)<\/think>/g, (_, inner) => {
|
|
39
51
|
blocks.push(inner.trim());
|
|
40
52
|
return '';
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
});
|
|
54
|
+
const unclosed = answer.indexOf(OPEN_THINK);
|
|
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() };
|
|
44
62
|
}
|
|
45
|
-
const OPEN_THINK = '<think>';
|
|
46
|
-
const CLOSE_THINK = '</think>';
|
|
47
63
|
/** Length of the longest suffix of `s` that is a proper prefix of `tag`. */
|
|
48
64
|
function partialTagSuffix(s, tag) {
|
|
49
65
|
const max = Math.min(s.length, tag.length - 1);
|
|
@@ -140,10 +156,20 @@ async function* sseChunks(res) {
|
|
|
140
156
|
// ── OpenAiCompatibleLlm ──────────────────────────────────────────────────────
|
|
141
157
|
export class OpenAiCompatibleLlm extends BaseLlm {
|
|
142
158
|
webSearchWarned = false;
|
|
159
|
+
retryWarned = false;
|
|
143
160
|
/** Model id as the wire expects it (e.g. strip the "ollama/" namespace). */
|
|
144
161
|
wireModelName() {
|
|
145
162
|
return this.model;
|
|
146
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
|
+
}
|
|
147
173
|
/** Provider-specific request body fields (merged last). */
|
|
148
174
|
/** Whether the endpoint accepts response_format json_schema (strict). */
|
|
149
175
|
supportsJsonSchemaFormat() {
|
|
@@ -191,6 +217,34 @@ export class OpenAiCompatibleLlm extends BaseLlm {
|
|
|
191
217
|
errorMessage: `${this.providerId()} returned ${status}: ${detail.slice(0, 4000)}`,
|
|
192
218
|
};
|
|
193
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
|
+
};
|
|
247
|
+
}
|
|
194
248
|
/** Error response when the endpoint can't be reached at all. */
|
|
195
249
|
unreachable(message) {
|
|
196
250
|
return {
|
|
@@ -202,12 +256,49 @@ export class OpenAiCompatibleLlm extends BaseLlm {
|
|
|
202
256
|
async *generateContentAsync(llmRequest, stream = false) {
|
|
203
257
|
yield* traceLlmGeneration({ provider: this.providerId(), model: this.model, llmRequest }, this.generateInner(llmRequest, stream));
|
|
204
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
|
+
*/
|
|
205
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) {
|
|
206
296
|
const missing = this.missingRequirement();
|
|
207
297
|
if (missing) {
|
|
208
298
|
yield missing;
|
|
209
299
|
return;
|
|
210
300
|
}
|
|
301
|
+
setLlmSpanAttribute('llm.transport', this.transport());
|
|
211
302
|
const messages = this.buildMessages(llmRequest);
|
|
212
303
|
const openAiTools = this.buildTools(llmRequest);
|
|
213
304
|
const cfg = llmRequest.config ?? {};
|
|
@@ -229,8 +320,8 @@ export class OpenAiCompatibleLlm extends BaseLlm {
|
|
|
229
320
|
// and return an empty turn. Ollama's OpenAI-compatible endpoint honors
|
|
230
321
|
// `reasoning_effort` and IGNORES the native `think` field, so this is
|
|
231
322
|
// the only lever on this path. Opt-in per agent; omitted = unchanged.
|
|
232
|
-
...(cfg.reasoningEffort !== undefined
|
|
233
|
-
? { reasoning_effort: cfg.reasoningEffort }
|
|
323
|
+
...((reasoningEffort ?? cfg.reasoningEffort) !== undefined
|
|
324
|
+
? { reasoning_effort: reasoningEffort ?? cfg.reasoningEffort }
|
|
234
325
|
: {}),
|
|
235
326
|
...(openAiTools.length > 0 ? { tools: openAiTools } : {}),
|
|
236
327
|
// Structured output: an ADK outputSchema becomes a strict json_schema
|
|
@@ -253,22 +344,34 @@ export class OpenAiCompatibleLlm extends BaseLlm {
|
|
|
253
344
|
}
|
|
254
345
|
else {
|
|
255
346
|
setLlmSpanAttribute('llm.web_search.omitted', true);
|
|
347
|
+
setLlmSpanAttribute('llm.capability.dropped', 'web_search');
|
|
256
348
|
if (!this.webSearchWarned) {
|
|
257
349
|
this.webSearchWarned = true;
|
|
258
|
-
|
|
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 — ` +
|
|
259
354
|
'tool omitted (the agent runs without search).');
|
|
260
355
|
}
|
|
261
356
|
}
|
|
262
357
|
}
|
|
263
358
|
try {
|
|
264
|
-
|
|
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(), {
|
|
265
364
|
method: 'POST',
|
|
266
365
|
headers: { 'Content-Type': 'application/json', ...this.headers() },
|
|
267
366
|
body: JSON.stringify(body),
|
|
367
|
+
signal,
|
|
368
|
+
}, {
|
|
369
|
+
signal,
|
|
370
|
+
onRetry: ({ retries }) => setLlmSpanAttribute('llm.retries', retries),
|
|
268
371
|
});
|
|
269
372
|
if (!res.ok) {
|
|
270
373
|
const detail = await res.text().catch(() => '');
|
|
271
|
-
yield this.httpError(res.status, detail);
|
|
374
|
+
yield withHttpStatus(this.httpError(res.status, detail), res.status);
|
|
272
375
|
return;
|
|
273
376
|
}
|
|
274
377
|
if (stream) {
|
|
@@ -276,7 +379,8 @@ export class OpenAiCompatibleLlm extends BaseLlm {
|
|
|
276
379
|
return;
|
|
277
380
|
}
|
|
278
381
|
const data = await res.json();
|
|
279
|
-
const
|
|
382
|
+
const choice = data.choices?.[0] ?? {};
|
|
383
|
+
const message = choice.message ?? {};
|
|
280
384
|
const usageMetadata = mapUsage(data.usage);
|
|
281
385
|
const { reasoning, answer } = this.extractReasoning(message);
|
|
282
386
|
if (reasoning) {
|
|
@@ -308,11 +412,7 @@ export class OpenAiCompatibleLlm extends BaseLlm {
|
|
|
308
412
|
},
|
|
309
413
|
});
|
|
310
414
|
}
|
|
311
|
-
yield
|
|
312
|
-
content: { role: 'model', parts },
|
|
313
|
-
turnComplete: true,
|
|
314
|
-
...(usageMetadata ? { usageMetadata } : {}),
|
|
315
|
-
};
|
|
415
|
+
yield this.finalResponse(parts, choice.finish_reason, Boolean(reasoning), usageMetadata);
|
|
316
416
|
}
|
|
317
417
|
catch (err) {
|
|
318
418
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -338,20 +438,29 @@ export class OpenAiCompatibleLlm extends BaseLlm {
|
|
|
338
438
|
const toolCalls = new Map();
|
|
339
439
|
let answer = '';
|
|
340
440
|
let answerStarted = false;
|
|
441
|
+
let sawReasoning = false;
|
|
442
|
+
let finishReason;
|
|
341
443
|
let usage;
|
|
342
|
-
const partial = (text, thought) =>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
};
|
|
349
455
|
// The scratchpad is usually trailed by blank lines; the reply must not
|
|
350
456
|
// open with them, but only the FIRST answer text may be trimmed.
|
|
351
457
|
const openAnswer = (text) => answerStarted ? text : text.trimStart();
|
|
352
458
|
for await (const chunk of sseChunks(res)) {
|
|
353
459
|
if (chunk.usage)
|
|
354
460
|
usage = chunk.usage;
|
|
461
|
+
const finish = chunk.choices?.[0]?.finish_reason;
|
|
462
|
+
if (finish)
|
|
463
|
+
finishReason = finish;
|
|
355
464
|
const delta = chunk.choices?.[0]?.delta;
|
|
356
465
|
if (!delta)
|
|
357
466
|
continue;
|
|
@@ -409,11 +518,38 @@ export class OpenAiCompatibleLlm extends BaseLlm {
|
|
|
409
518
|
}
|
|
410
519
|
parts.push({ functionCall: { name: acc.name, args, id: acc.id } });
|
|
411
520
|
}
|
|
412
|
-
|
|
413
|
-
|
|
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 {
|
|
414
549
|
content: { role: 'model', parts },
|
|
415
550
|
turnComplete: true,
|
|
416
|
-
...(
|
|
551
|
+
...(truncated ? { finishReason: FinishReason.MAX_TOKENS } : {}),
|
|
552
|
+
...usage,
|
|
417
553
|
};
|
|
418
554
|
}
|
|
419
555
|
// ── Translation helpers ────────────────────────────────────────────────────
|
|
@@ -512,14 +648,14 @@ export class OpenAiCompatibleLlm extends BaseLlm {
|
|
|
512
648
|
for (const [, tool] of Object.entries(llmRequest.toolsDict ?? {})) {
|
|
513
649
|
if (isWebSearchSentinel(tool))
|
|
514
650
|
continue; // handled via body fields
|
|
515
|
-
const
|
|
516
|
-
if (
|
|
651
|
+
const decl = toolDeclarationFor(tool);
|
|
652
|
+
if (decl) {
|
|
517
653
|
openAiTools.push({
|
|
518
654
|
type: 'function',
|
|
519
655
|
function: {
|
|
520
|
-
name:
|
|
521
|
-
description:
|
|
522
|
-
parameters:
|
|
656
|
+
name: decl.name,
|
|
657
|
+
description: decl.description,
|
|
658
|
+
parameters: decl.parameters,
|
|
523
659
|
},
|
|
524
660
|
});
|
|
525
661
|
}
|
|
@@ -535,7 +671,34 @@ export class OpenAiCompatibleLlm extends BaseLlm {
|
|
|
535
671
|
'Use a Gemini model for live/streaming sessions.');
|
|
536
672
|
}
|
|
537
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Stamps an HTTP error response with its numeric status and whether that
|
|
676
|
+
* status is transient, so callers above (and the ledger, via the tracer's
|
|
677
|
+
* llm.payload.response) can tell a rate limit from a bad request without
|
|
678
|
+
* parsing the message. Applied in the base so a subclass's httpError
|
|
679
|
+
* override (Ollama's hint, the gateway's GATEWAY_HTTP_ERROR) keeps its
|
|
680
|
+
* errorCode and wording and still gets the fields.
|
|
681
|
+
*/
|
|
682
|
+
export function withHttpStatus(resp, status) {
|
|
683
|
+
setLlmSpanAttribute('llm.http_status', status);
|
|
684
|
+
return { ...resp, status, retryable: isRetryableStatus(status) };
|
|
685
|
+
}
|
|
538
686
|
/** OpenAI-style usage → GenAI usageMetadata (undefined when absent). */
|
|
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. */
|
|
690
|
+
export function addUsage(a, b) {
|
|
691
|
+
if (!a)
|
|
692
|
+
return b;
|
|
693
|
+
if (!b)
|
|
694
|
+
return a;
|
|
695
|
+
const out = {};
|
|
696
|
+
for (const k of ['promptTokenCount', 'candidatesTokenCount', 'thoughtsTokenCount', 'totalTokenCount']) {
|
|
697
|
+
if (a[k] !== undefined || b[k] !== undefined)
|
|
698
|
+
out[k] = (a[k] ?? 0) + (b[k] ?? 0);
|
|
699
|
+
}
|
|
700
|
+
return out;
|
|
701
|
+
}
|
|
539
702
|
export function mapUsage(usage) {
|
|
540
703
|
if (!usage || typeof usage !== 'object')
|
|
541
704
|
return undefined;
|
|
@@ -39,7 +39,14 @@ export function providerKeyPresent(provider) {
|
|
|
39
39
|
return true;
|
|
40
40
|
// Gemini historically accepts either env name.
|
|
41
41
|
if (provider === 'gemini') {
|
|
42
|
-
return
|
|
42
|
+
return realValue(process.env.GOOGLE_GENAI_API_KEY) || realValue(process.env.GEMINI_API_KEY);
|
|
43
43
|
}
|
|
44
|
-
return
|
|
44
|
+
return realValue(process.env[keyEnv]);
|
|
45
|
+
}
|
|
46
|
+
/** Set, and not a `.env.example` placeholder (`your_..._here`, `<...>`).
|
|
47
|
+
* Inlined rather than imported: this module stays a dependency-free leaf. */
|
|
48
|
+
function realValue(v) {
|
|
49
|
+
if (!v || !v.trim())
|
|
50
|
+
return false;
|
|
51
|
+
return !/^your[_-].*[_-]here$/i.test(v.trim()) && !/^<[^>]*>$/.test(v.trim());
|
|
45
52
|
}
|
|
@@ -23,22 +23,51 @@
|
|
|
23
23
|
* resolution (LRU), so late registration can be masked by stale cache hits.
|
|
24
24
|
*/
|
|
25
25
|
import { Gemini } from '@google/adk';
|
|
26
|
-
import type { BaseLlm, LlmRequest, LlmResponse } from '@google/adk';
|
|
26
|
+
import type { BaseLlm, GeminiParams, LlmRequest, LlmResponse } from '@google/adk';
|
|
27
|
+
import type { ProviderEndpoint } from './endpoints.ts';
|
|
28
|
+
import { GatewayLlm } from './gatewayLlm.ts';
|
|
27
29
|
import { PROVIDERS, providerForModel, providerKeyPresent } from './providerMap.ts';
|
|
28
30
|
import type { ProviderId } from './providerMap.ts';
|
|
29
31
|
export { providerForModel, providerKeyPresent, PROVIDERS };
|
|
32
|
+
export { endpointFromEnv, endpointLabel, endpointProblems, PLATFORMS_FOR, platformFromEnv, providerReady, } from './endpoints.ts';
|
|
33
|
+
export type { Platform, ProviderEndpoint } from './endpoints.ts';
|
|
30
34
|
export type { ProviderId };
|
|
35
|
+
export { gatewayConfig, gatewayProblem, gatewayUsable, planTransport, GATEWAYS } from './gateway.ts';
|
|
36
|
+
export type { GatewayId, GatewayInfo, TransportPlan } from './gateway.ts';
|
|
37
|
+
export { describeCapabilities, capabilitySummary } from './capabilities.ts';
|
|
38
|
+
export type { CapabilityReport } from './capabilities.ts';
|
|
39
|
+
export { GatewayLlm };
|
|
31
40
|
export declare class TracedGemini extends Gemini {
|
|
32
41
|
static readonly supportedModels: Array<string | RegExp>;
|
|
33
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The Gemini platform (ADR 0023) applies to every construction, the
|
|
44
|
+
* LLMRegistry's included: on Vertex AI the client authenticates with
|
|
45
|
+
* Google Application Default Credentials for the configured project and
|
|
46
|
+
* location, and an AI Studio key (the environment's or a caller's) is not
|
|
47
|
+
* sent. `endpoint` overrides the environment's (the credentials plug point).
|
|
48
|
+
*/
|
|
49
|
+
constructor(params?: GeminiParams & {
|
|
50
|
+
endpoint?: ProviderEndpoint;
|
|
51
|
+
});
|
|
52
|
+
generateContentAsync(llmRequest: LlmRequest, stream?: boolean, abortSignal?: AbortSignal): AsyncGenerator<LlmResponse, void>;
|
|
34
53
|
/** Tags the llm.request span (web_search = Gemini grounding), then
|
|
35
|
-
* delegates to ADK's Gemini. Runs inside the span context.
|
|
54
|
+
* delegates to ADK's Gemini. Runs inside the span context. The abort
|
|
55
|
+
* signal is forwarded so a canceled turn stops the request in flight
|
|
56
|
+
* (ADK's Gemini puts it on the genai request config). */
|
|
36
57
|
private tagAndGenerate;
|
|
37
58
|
}
|
|
38
59
|
export interface ProviderStatus {
|
|
39
60
|
provider: ProviderId;
|
|
40
61
|
label: string;
|
|
41
62
|
available: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* How this provider's ids will be served: its own endpoint, or the
|
|
65
|
+
* configured gateway standing in because the direct key is absent
|
|
66
|
+
* (lib/models/gateway.ts). Absent when unavailable.
|
|
67
|
+
*/
|
|
68
|
+
transport?: 'direct' | 'gateway';
|
|
69
|
+
/** Gateway id when transport is 'gateway'. */
|
|
70
|
+
gateway?: string;
|
|
42
71
|
/** Why the provider is unavailable (e.g. which env var is missing). */
|
|
43
72
|
reason?: string;
|
|
44
73
|
}
|
|
@@ -48,8 +77,10 @@ export declare function providerStatuses(): ProviderStatus[];
|
|
|
48
77
|
* Registers every adapter whose credentials exist into the ADK LLMRegistry
|
|
49
78
|
* (Ollama unconditionally — local needs no key; Gemini always, since the
|
|
50
79
|
* registry needs a fallback class, and a missing Gemini key surfaces as a
|
|
51
|
-
* clear API error at call time).
|
|
52
|
-
*
|
|
80
|
+
* clear API error at call time). When MODEL_GATEWAY is configured, a
|
|
81
|
+
* provider whose direct key is ABSENT is served by the gateway stand-in
|
|
82
|
+
* instead (lib/models/gateway.ts — the fallback rule). Returns the
|
|
83
|
+
* per-provider statuses so entrypoints can log which models are routable.
|
|
53
84
|
*/
|
|
54
85
|
export declare function registerAvailableProviders(log?: (msg: string) => void): ProviderStatus[];
|
|
55
86
|
export interface ResolveModelOptions {
|
|
@@ -61,6 +92,12 @@ export interface ResolveModelOptions {
|
|
|
61
92
|
* YAML always wins.
|
|
62
93
|
*/
|
|
63
94
|
defaultProvider?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Where this request goes and how it authenticates (ADR 0023), merged over
|
|
97
|
+
* the environment's endpoint for the model's provider: the credentials plug
|
|
98
|
+
* point's per-request answer.
|
|
99
|
+
*/
|
|
100
|
+
endpoint?: Partial<ProviderEndpoint>;
|
|
64
101
|
}
|
|
65
102
|
/**
|
|
66
103
|
* Resolves a model id (from YAML) to a provider adapter INSTANCE, injecting
|