@ixo/oracle-runtime-workers 0.2.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/README.md +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- package/src/tasks/store.ts +351 -0
package/src/core/llm.ts
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
2
|
+
import { LangChainTracer } from '@langchain/core/tracers/tracer_langchain';
|
|
3
|
+
import { ChatOpenAI } from '@langchain/openai';
|
|
4
|
+
import { Client } from 'langsmith';
|
|
5
|
+
import type { ChatOpenAIFields, Logger, ModelRole } from '../plugin-api/types';
|
|
6
|
+
import { fetchOpenRouterPrices } from './openrouter-pricing';
|
|
7
|
+
import type { LlmAdapter } from './runtime-context';
|
|
8
|
+
import { NOOP_LOGGER } from './utils';
|
|
9
|
+
|
|
10
|
+
// ── Catalog ─────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/** Coarse capability/price tier. Drives the `$`/`$$`/`$$$` + badge shown to users. */
|
|
13
|
+
export type ModelTier = 'everyday' | 'balanced' | 'top';
|
|
14
|
+
|
|
15
|
+
/** Provider family — lets a picker group models or show a logo. */
|
|
16
|
+
export type ModelFamily =
|
|
17
|
+
| 'openai'
|
|
18
|
+
| 'google'
|
|
19
|
+
| 'anthropic'
|
|
20
|
+
| 'moonshotai'
|
|
21
|
+
| 'z-ai'
|
|
22
|
+
| 'deepseek';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Native (non-text) input types a model accepts. Drives attachment routing:
|
|
26
|
+
* an attachment is sent to the model directly only when the model accepts
|
|
27
|
+
* that modality.
|
|
28
|
+
*/
|
|
29
|
+
export interface ModelInputCapabilities {
|
|
30
|
+
image: boolean;
|
|
31
|
+
file: boolean;
|
|
32
|
+
audio: boolean;
|
|
33
|
+
video: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A single hand-curated model. */
|
|
37
|
+
export interface ModelCatalogEntry {
|
|
38
|
+
/** OpenRouter slug — sent to the provider and used as the allow-list key. */
|
|
39
|
+
id: string;
|
|
40
|
+
/** Friendly display name. */
|
|
41
|
+
label: string;
|
|
42
|
+
family: ModelFamily;
|
|
43
|
+
tier: ModelTier;
|
|
44
|
+
/** One plain-language sentence: when should someone pick this model? */
|
|
45
|
+
blurb: string;
|
|
46
|
+
/** Whether the model accepts image input. */
|
|
47
|
+
vision: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Baseline list price, used when live OpenRouter pricing is unavailable
|
|
50
|
+
* (see `openrouter-pricing.ts`). Raw provider price; markup applied later.
|
|
51
|
+
*/
|
|
52
|
+
baselinePrice: ModelPrice;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Raw list price, USD per million tokens. */
|
|
56
|
+
export interface ModelPrice {
|
|
57
|
+
inputPerMillion: number;
|
|
58
|
+
outputPerMillion: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The default model for a fresh chat. An oracle operator can override this
|
|
63
|
+
* per deployment via the `DEFAULT_MODEL` env var (see `getDefaultModelId`).
|
|
64
|
+
*/
|
|
65
|
+
export const DEFAULT_MODEL_ID = 'openai/gpt-5.6-luna';
|
|
66
|
+
|
|
67
|
+
/** Per-tier presentation, defined once so the wording stays consistent. */
|
|
68
|
+
export const TIER_DISPLAY: Record<
|
|
69
|
+
ModelTier,
|
|
70
|
+
{ costLabel: string; badge: string; order: number }
|
|
71
|
+
> = {
|
|
72
|
+
everyday: { costLabel: '$', badge: 'Fast', order: 0 },
|
|
73
|
+
balanced: { costLabel: '$$', badge: 'Balanced', order: 1 },
|
|
74
|
+
top: { costLabel: '$$$', badge: 'Smartest', order: 2 },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The curated, user-facing model catalog — also the **allow-list**: a
|
|
79
|
+
* per-request model override is only honoured if its id appears here.
|
|
80
|
+
* `baselinePrice` is the fallback when live OpenRouter pricing is unavailable.
|
|
81
|
+
*/
|
|
82
|
+
export const MODEL_CATALOG: readonly ModelCatalogEntry[] = [
|
|
83
|
+
// ── $ Everyday — fast and cheap ─────────────────────────────────────────
|
|
84
|
+
{
|
|
85
|
+
id: DEFAULT_MODEL_ID,
|
|
86
|
+
label: 'GPT-5.4 Nano',
|
|
87
|
+
family: 'openai',
|
|
88
|
+
tier: 'everyday',
|
|
89
|
+
blurb: 'Fast and low-cost — great for everyday questions and quick help.',
|
|
90
|
+
vision: true,
|
|
91
|
+
baselinePrice: { inputPerMillion: 0.2, outputPerMillion: 1.2 },
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'google/gemini-3.1-flash-lite',
|
|
95
|
+
label: 'Gemini 3.1 Flash Lite',
|
|
96
|
+
family: 'google',
|
|
97
|
+
tier: 'everyday',
|
|
98
|
+
blurb: 'Speedy and inexpensive, and it can read images too.',
|
|
99
|
+
vision: true,
|
|
100
|
+
baselinePrice: { inputPerMillion: 0.25, outputPerMillion: 1.5 },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'z-ai/glm-5.2',
|
|
104
|
+
label: 'GLM 5.2',
|
|
105
|
+
family: 'z-ai',
|
|
106
|
+
tier: 'everyday',
|
|
107
|
+
blurb: 'Budget-friendly open model that handles general chat well.',
|
|
108
|
+
vision: false,
|
|
109
|
+
baselinePrice: { inputPerMillion: 0.2968, outputPerMillion: 0.9328 },
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'moonshotai/kimi-k2.5',
|
|
113
|
+
label: 'Kimi K2.5',
|
|
114
|
+
family: 'moonshotai',
|
|
115
|
+
tier: 'everyday',
|
|
116
|
+
blurb: 'Low-cost open model that can also look at images.',
|
|
117
|
+
vision: true,
|
|
118
|
+
baselinePrice: { inputPerMillion: 0.57, outputPerMillion: 2.85 },
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// ── $$ Balanced — smarter, still affordable ─────────────────────────────
|
|
122
|
+
{
|
|
123
|
+
id: 'openai/gpt-5.6-luna',
|
|
124
|
+
label: 'GPT-5.6 Luna',
|
|
125
|
+
family: 'openai',
|
|
126
|
+
tier: 'balanced',
|
|
127
|
+
blurb: 'A smart all-rounder that balances speed and reasoning.',
|
|
128
|
+
vision: true,
|
|
129
|
+
baselinePrice: { inputPerMillion: 1.0, outputPerMillion: 6.0 },
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'google/gemini-3.5-flash',
|
|
133
|
+
label: 'Gemini 3.5 Flash',
|
|
134
|
+
family: 'google',
|
|
135
|
+
tier: 'balanced',
|
|
136
|
+
blurb: 'Capable multimodal model for tougher, more detailed tasks.',
|
|
137
|
+
vision: true,
|
|
138
|
+
baselinePrice: { inputPerMillion: 1.5, outputPerMillion: 9.0 },
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'anthropic/claude-sonnet-5',
|
|
142
|
+
label: 'Claude Sonnet 5',
|
|
143
|
+
family: 'anthropic',
|
|
144
|
+
tier: 'balanced',
|
|
145
|
+
blurb: 'Great at careful writing, reasoning and coding.',
|
|
146
|
+
vision: true,
|
|
147
|
+
baselinePrice: { inputPerMillion: 2.0, outputPerMillion: 10.0 },
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// ── $$$ Top-tier — most capable ─────────────────────────────────────────
|
|
151
|
+
{
|
|
152
|
+
id: 'moonshotai/kimi-k3',
|
|
153
|
+
label: 'Kimi K3',
|
|
154
|
+
family: 'moonshotai',
|
|
155
|
+
tier: 'top',
|
|
156
|
+
blurb: 'Powerful open model for complex, long-running tasks.',
|
|
157
|
+
vision: true,
|
|
158
|
+
baselinePrice: { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'anthropic/claude-opus-4.8',
|
|
162
|
+
label: 'Claude Opus 4.8',
|
|
163
|
+
family: 'anthropic',
|
|
164
|
+
tier: 'top',
|
|
165
|
+
blurb: "Anthropic's most capable model for hard problems.",
|
|
166
|
+
vision: true,
|
|
167
|
+
baselinePrice: { inputPerMillion: 5.0, outputPerMillion: 25.0 },
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'openai/gpt-5.6-sol',
|
|
171
|
+
label: 'GPT-5.6 Sol',
|
|
172
|
+
family: 'openai',
|
|
173
|
+
tier: 'top',
|
|
174
|
+
blurb: "OpenAI's flagship for the most complex work.",
|
|
175
|
+
vision: true,
|
|
176
|
+
baselinePrice: { inputPerMillion: 5.0, outputPerMillion: 30.0 },
|
|
177
|
+
},
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
/** The public, per-model shape returned by a `GET /models` route. */
|
|
181
|
+
export interface ModelListItem {
|
|
182
|
+
id: string;
|
|
183
|
+
label: string;
|
|
184
|
+
family: ModelFamily;
|
|
185
|
+
tier: ModelTier;
|
|
186
|
+
/** `$` / `$$` / `$$$` — the at-a-glance cost cue. */
|
|
187
|
+
costLabel: string;
|
|
188
|
+
/** `Fast` / `Balanced` / `Smartest`. */
|
|
189
|
+
badge: string;
|
|
190
|
+
blurb: string;
|
|
191
|
+
vision: boolean;
|
|
192
|
+
/**
|
|
193
|
+
* The price the user pays, already including the platform markup. The raw
|
|
194
|
+
* provider price and the markup multiplier are deliberately NOT included.
|
|
195
|
+
*/
|
|
196
|
+
pricing: {
|
|
197
|
+
inputPerMillion: number;
|
|
198
|
+
outputPerMillion: number;
|
|
199
|
+
currency: 'USD';
|
|
200
|
+
unit: 'per_million_tokens';
|
|
201
|
+
};
|
|
202
|
+
/** True for the model a fresh chat uses when the user hasn't picked one. */
|
|
203
|
+
isDefault: boolean;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The full `GET /models` response — the Node runtime's `ModelListing`. */
|
|
207
|
+
export interface ModelListing {
|
|
208
|
+
models: ModelListItem[];
|
|
209
|
+
/** Id of the default model (also flagged via `isDefault` on the item). */
|
|
210
|
+
default: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const catalogById = new Map(MODEL_CATALOG.map((m) => [m.id, m]));
|
|
214
|
+
|
|
215
|
+
/** Look up a catalog entry by its OpenRouter id. */
|
|
216
|
+
export function getCatalogEntry(id: string): ModelCatalogEntry | undefined {
|
|
217
|
+
return catalogById.get(id);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Whether `id` is a selectable model. This is the allow-list guard for the
|
|
222
|
+
* per-request override — an unknown id is rejected and the turn falls back to
|
|
223
|
+
* the default model.
|
|
224
|
+
*/
|
|
225
|
+
export function isAllowedModel(id: string | undefined | null): id is string {
|
|
226
|
+
return typeof id === 'string' && catalogById.has(id);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The effective default model id for this deployment: the `DEFAULT_MODEL`
|
|
231
|
+
* config value when an operator has set one, otherwise {@link DEFAULT_MODEL_ID}.
|
|
232
|
+
* Reads the validated config, never `process.env`.
|
|
233
|
+
*/
|
|
234
|
+
export function getDefaultModelId(
|
|
235
|
+
config: { DEFAULT_MODEL?: unknown } | undefined,
|
|
236
|
+
): string {
|
|
237
|
+
const raw = config?.DEFAULT_MODEL;
|
|
238
|
+
const override = typeof raw === 'string' ? raw.trim() : '';
|
|
239
|
+
return override.length > 0 ? override : DEFAULT_MODEL_ID;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const MODEL_INPUT_CAPS: Record<string, ModelInputCapabilities> = {
|
|
243
|
+
'openai/gpt-5.4-nano': {
|
|
244
|
+
image: true,
|
|
245
|
+
file: true,
|
|
246
|
+
audio: false,
|
|
247
|
+
video: false,
|
|
248
|
+
},
|
|
249
|
+
'google/gemini-3.1-flash-lite': {
|
|
250
|
+
image: true,
|
|
251
|
+
file: true,
|
|
252
|
+
audio: false,
|
|
253
|
+
video: false,
|
|
254
|
+
},
|
|
255
|
+
'z-ai/glm-5.2': { image: false, file: false, audio: false, video: false },
|
|
256
|
+
'moonshotai/kimi-k2.5': {
|
|
257
|
+
image: true,
|
|
258
|
+
file: false,
|
|
259
|
+
audio: false,
|
|
260
|
+
video: false,
|
|
261
|
+
},
|
|
262
|
+
'openai/gpt-5.6-luna': {
|
|
263
|
+
image: true,
|
|
264
|
+
file: true,
|
|
265
|
+
audio: false,
|
|
266
|
+
video: false,
|
|
267
|
+
},
|
|
268
|
+
'google/gemini-3.5-flash': {
|
|
269
|
+
image: true,
|
|
270
|
+
file: true,
|
|
271
|
+
audio: false,
|
|
272
|
+
video: false,
|
|
273
|
+
},
|
|
274
|
+
'anthropic/claude-sonnet-5': {
|
|
275
|
+
image: true,
|
|
276
|
+
file: true,
|
|
277
|
+
audio: false,
|
|
278
|
+
video: false,
|
|
279
|
+
},
|
|
280
|
+
'moonshotai/kimi-k3': {
|
|
281
|
+
image: true,
|
|
282
|
+
file: false,
|
|
283
|
+
audio: false,
|
|
284
|
+
video: false,
|
|
285
|
+
},
|
|
286
|
+
'anthropic/claude-opus-4.8': {
|
|
287
|
+
image: true,
|
|
288
|
+
file: true,
|
|
289
|
+
audio: false,
|
|
290
|
+
video: false,
|
|
291
|
+
},
|
|
292
|
+
'openai/gpt-5.6-sol': { image: true, file: true, audio: false, video: false },
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const TEXT_ONLY_CAPS: ModelInputCapabilities = {
|
|
296
|
+
image: false,
|
|
297
|
+
file: false,
|
|
298
|
+
audio: false,
|
|
299
|
+
video: false,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/** Native input capabilities for a model id; text-only for unknown ids. */
|
|
303
|
+
export function getModelCapabilities(modelId: string): ModelInputCapabilities {
|
|
304
|
+
return MODEL_INPUT_CAPS[modelId] ?? TEXT_ONLY_CAPS;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The display markup applied to raw provider prices when none is configured. */
|
|
308
|
+
export const DEFAULT_MODEL_PRICE_MARKUP = 1.6;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The display markup: `MODEL_PRICE_MARKUP` when it is a positive finite number
|
|
312
|
+
* (validated config or the raw Worker `env` string), else the default.
|
|
313
|
+
*/
|
|
314
|
+
export function getModelPriceMarkup(
|
|
315
|
+
config: { MODEL_PRICE_MARKUP?: unknown } | undefined,
|
|
316
|
+
): number {
|
|
317
|
+
const markup = Number(config?.MODEL_PRICE_MARKUP);
|
|
318
|
+
return Number.isFinite(markup) && markup > 0
|
|
319
|
+
? markup
|
|
320
|
+
: DEFAULT_MODEL_PRICE_MARKUP;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function roundPrice(value: number): number {
|
|
324
|
+
return Math.round(value * 10_000) / 10_000;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Build the public listing from the catalog, applying `markup` to each price
|
|
329
|
+
* (live price when available in `livePrices`, else the entry's baseline).
|
|
330
|
+
* Pure — same as the Node runtime's `buildModelListing`; the caller supplies
|
|
331
|
+
* the live price map, markup, and default id.
|
|
332
|
+
*/
|
|
333
|
+
export function buildModelListing(params: {
|
|
334
|
+
livePrices?: ReadonlyMap<string, ModelPrice>;
|
|
335
|
+
markup: number;
|
|
336
|
+
defaultModelId: string;
|
|
337
|
+
}): ModelListing {
|
|
338
|
+
const { livePrices, markup, defaultModelId } = params;
|
|
339
|
+
|
|
340
|
+
const models: ModelListItem[] = MODEL_CATALOG.map((entry): ModelListItem => {
|
|
341
|
+
const raw = livePrices?.get(entry.id) ?? entry.baselinePrice;
|
|
342
|
+
const display = TIER_DISPLAY[entry.tier];
|
|
343
|
+
return {
|
|
344
|
+
id: entry.id,
|
|
345
|
+
label: entry.label,
|
|
346
|
+
family: entry.family,
|
|
347
|
+
tier: entry.tier,
|
|
348
|
+
costLabel: display.costLabel,
|
|
349
|
+
badge: display.badge,
|
|
350
|
+
blurb: entry.blurb,
|
|
351
|
+
vision: entry.vision,
|
|
352
|
+
pricing: {
|
|
353
|
+
inputPerMillion: roundPrice(raw.inputPerMillion * markup),
|
|
354
|
+
outputPerMillion: roundPrice(raw.outputPerMillion * markup),
|
|
355
|
+
currency: 'USD',
|
|
356
|
+
unit: 'per_million_tokens',
|
|
357
|
+
},
|
|
358
|
+
isDefault: entry.id === defaultModelId,
|
|
359
|
+
};
|
|
360
|
+
}).sort(
|
|
361
|
+
(a, b) =>
|
|
362
|
+
TIER_DISPLAY[a.tier].order - TIER_DISPLAY[b.tier].order ||
|
|
363
|
+
a.pricing.inputPerMillion - b.pricing.inputPerMillion,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
return { models, default: defaultModelId };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The `GET /models` payload without a network call: the curated catalog priced
|
|
371
|
+
* from `livePrices` (else baselines) with the configured markup and the
|
|
372
|
+
* deployment default flagged. `env` is the Worker `env` (or validated config)
|
|
373
|
+
* — only `DEFAULT_MODEL` and `MODEL_PRICE_MARKUP` are read.
|
|
374
|
+
*/
|
|
375
|
+
export function listModelCatalog(
|
|
376
|
+
env: { DEFAULT_MODEL?: unknown; MODEL_PRICE_MARKUP?: unknown } | undefined,
|
|
377
|
+
livePrices?: ReadonlyMap<string, ModelPrice>,
|
|
378
|
+
): ModelListing {
|
|
379
|
+
return buildModelListing({
|
|
380
|
+
livePrices,
|
|
381
|
+
markup: getModelPriceMarkup(env),
|
|
382
|
+
defaultModelId: getDefaultModelId(env),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The `GET /models` payload with live OpenRouter prices (cached per isolate
|
|
388
|
+
* for an hour; baseline prices when the fetch fails) — what the Node runtime's
|
|
389
|
+
* `ModelsService.listModels` returns.
|
|
390
|
+
*/
|
|
391
|
+
export async function listModels(
|
|
392
|
+
env:
|
|
393
|
+
| {
|
|
394
|
+
DEFAULT_MODEL?: unknown;
|
|
395
|
+
MODEL_PRICE_MARKUP?: unknown;
|
|
396
|
+
OPEN_ROUTER_API_KEY?: unknown;
|
|
397
|
+
}
|
|
398
|
+
| undefined,
|
|
399
|
+
): Promise<ModelListing> {
|
|
400
|
+
const apiKey =
|
|
401
|
+
typeof env?.OPEN_ROUTER_API_KEY === 'string' && env.OPEN_ROUTER_API_KEY
|
|
402
|
+
? env.OPEN_ROUTER_API_KEY
|
|
403
|
+
: undefined;
|
|
404
|
+
const livePrices = await fetchOpenRouterPrices({ apiKey });
|
|
405
|
+
return listModelCatalog(env, livePrices);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ── Role → model map (OpenRouter only) ──────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Every role the provider maps to a model id. The plugin-API exposes a lean
|
|
412
|
+
* `ModelRole` (`'main' | 'subagent' | 'utility' | string`); the adapter falls
|
|
413
|
+
* back to `subagent` for unrecognized strings.
|
|
414
|
+
*/
|
|
415
|
+
export type ProviderModelRole =
|
|
416
|
+
| 'main'
|
|
417
|
+
| 'skills'
|
|
418
|
+
| 'subagent'
|
|
419
|
+
| 'vision'
|
|
420
|
+
| 'guard'
|
|
421
|
+
| 'routing'
|
|
422
|
+
| 'session-title'
|
|
423
|
+
| 'embedding'
|
|
424
|
+
| 'custom_medium'
|
|
425
|
+
| 'custom_low';
|
|
426
|
+
|
|
427
|
+
/** OpenRouter role map. `main` is resolved through `getDefaultModelId`. */
|
|
428
|
+
export const OPENROUTER_MODEL_MAP: Record<ProviderModelRole, string> = {
|
|
429
|
+
main: DEFAULT_MODEL_ID,
|
|
430
|
+
skills: 'openai/gpt-5.6-luna',
|
|
431
|
+
subagent: 'openai/gpt-5.6-luna',
|
|
432
|
+
vision: 'google/gemini-3.1-flash-lite',
|
|
433
|
+
guard: 'meta-llama/llama-3.1-8b-instruct',
|
|
434
|
+
routing: 'openai/gpt-oss-120b',
|
|
435
|
+
custom_low: 'openai/gpt-oss-120b',
|
|
436
|
+
custom_medium: 'google/gemini-3.1-flash-lite',
|
|
437
|
+
'session-title': 'meta-llama/llama-3.1-8b-instruct',
|
|
438
|
+
embedding: 'text-embedding-3-small',
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
/** OpenRouter fallback models for the 'main' role (via `models`, sorted by latency). */
|
|
442
|
+
export const OPENROUTER_MAIN_FALLBACKS: readonly string[] = [
|
|
443
|
+
'qwen/qwen3-235b-a22b-thinking-2507',
|
|
444
|
+
'google/gemini-2.5-flash-lite',
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
448
|
+
|
|
449
|
+
// ── Nebius (self-hosted deployments) ────────────────────────────────────────
|
|
450
|
+
|
|
451
|
+
export const NEBIUS_BASE_URL = 'https://api.tokenfactory.nebius.com/v1/';
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Nebius role map — a fixed map (mirrors the Node provider): only OpenRouter
|
|
455
|
+
* participates in `DEFAULT_MODEL` selection, so self-hosted deployments are
|
|
456
|
+
* unaffected by catalog changes.
|
|
457
|
+
*/
|
|
458
|
+
export const NEBIUS_MODEL_MAP: Record<ProviderModelRole, string> = {
|
|
459
|
+
main: 'Qwen/Qwen3-235B-A22B-Thinking-2507',
|
|
460
|
+
skills: 'Qwen/Qwen3-235B-A22B-Thinking-2507',
|
|
461
|
+
subagent: 'Qwen/Qwen3-235B-A22B-Instruct-2507',
|
|
462
|
+
vision: 'Qwen/Qwen2.5-VL-72B-Instruct',
|
|
463
|
+
guard: 'meta-llama/Llama-Guard-3-8B',
|
|
464
|
+
routing: 'Qwen/Qwen3-30B-A3B-Instruct-2507',
|
|
465
|
+
custom_low: '',
|
|
466
|
+
custom_medium: '',
|
|
467
|
+
'session-title': 'meta-llama/Meta-Llama-3.1-8B-Instruct',
|
|
468
|
+
embedding: 'Qwen/Qwen3-Embedding-8B',
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
/** Provider families the platform adapter can be built for. */
|
|
472
|
+
export type LlmProvider = 'openrouter' | 'nebius';
|
|
473
|
+
|
|
474
|
+
/** The slice of validated config the adapter reads. */
|
|
475
|
+
export interface LlmEnv {
|
|
476
|
+
OPEN_ROUTER_API_KEY: string;
|
|
477
|
+
DEFAULT_MODEL?: string;
|
|
478
|
+
MAIN_REASONING_EFFORT?: 'low' | 'medium' | 'high';
|
|
479
|
+
ORACLE_NAME?: string;
|
|
480
|
+
/** Platform provider — `openrouter` (default) or `nebius`. */
|
|
481
|
+
LLM_PROVIDER?: LlmProvider;
|
|
482
|
+
/** Required when `LLM_PROVIDER=nebius`. */
|
|
483
|
+
NEBIUS_API_KEY?: string;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Narrow the raw Worker `env` bindings to the adapter's env slice. Lets the
|
|
488
|
+
* host build a provider-selected adapter from `OracleWorkerEnv` directly
|
|
489
|
+
* (`LLM_PROVIDER` / `NEBIUS_API_KEY` are not part of the validated base
|
|
490
|
+
* schema); non-string / empty values read as unset.
|
|
491
|
+
*/
|
|
492
|
+
export function llmEnvFromWorkerEnv(env: Record<string, unknown>): LlmEnv {
|
|
493
|
+
const str = (key: string): string | undefined => {
|
|
494
|
+
const value = env[key];
|
|
495
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
496
|
+
};
|
|
497
|
+
const effort = str('MAIN_REASONING_EFFORT');
|
|
498
|
+
return {
|
|
499
|
+
OPEN_ROUTER_API_KEY: str('OPEN_ROUTER_API_KEY') ?? '',
|
|
500
|
+
DEFAULT_MODEL: str('DEFAULT_MODEL'),
|
|
501
|
+
MAIN_REASONING_EFFORT:
|
|
502
|
+
effort === 'low' || effort === 'medium' || effort === 'high'
|
|
503
|
+
? effort
|
|
504
|
+
: undefined,
|
|
505
|
+
ORACLE_NAME: str('ORACLE_NAME'),
|
|
506
|
+
LLM_PROVIDER: str('LLM_PROVIDER') === 'nebius' ? 'nebius' : 'openrouter',
|
|
507
|
+
NEBIUS_API_KEY: str('NEBIUS_API_KEY'),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Adapter surface — the ambient `llm` plus the role/model resolvers. The name
|
|
513
|
+
* predates provider selection; a Nebius-backed adapter satisfies the same
|
|
514
|
+
* interface (`providerConfig` then carries the Nebius base URL/key).
|
|
515
|
+
*/
|
|
516
|
+
export interface OpenRouterLlmAdapter extends LlmAdapter {
|
|
517
|
+
/** Model id a role resolves to (honours `DEFAULT_MODEL` for `main`). */
|
|
518
|
+
modelForRole(role: ModelRole): string;
|
|
519
|
+
/** The deployment's default `main` model id. */
|
|
520
|
+
readonly defaultModelId: string;
|
|
521
|
+
/** Provider config for raw fetch calls the host may make itself. */
|
|
522
|
+
readonly providerConfig: {
|
|
523
|
+
baseURL: string;
|
|
524
|
+
apiKey: string;
|
|
525
|
+
headers: Record<string, string>;
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
530
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
531
|
+
? (value as Record<string, unknown>)
|
|
532
|
+
: {};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Build the platform LLM adapter from validated config, selecting the
|
|
537
|
+
* provider by `LLM_PROVIDER` (`openrouter` default | `nebius`). `ChatOpenAI`
|
|
538
|
+
* speaks to both OpenAI-compatible endpoints over `fetch`, so it runs
|
|
539
|
+
* unchanged on Workers. Every model is created with `maxRetries: 2`.
|
|
540
|
+
*
|
|
541
|
+
* OpenRouter role handling mirrors the Node provider:
|
|
542
|
+
* - `main` → `DEFAULT_MODEL` (or the catalog default), with the OpenRouter
|
|
543
|
+
* `models` fallback array and `provider.sort: 'latency'`; reasoning
|
|
544
|
+
* effort from `MAIN_REASONING_EFFORT`.
|
|
545
|
+
* - every other role → the fixed map; unknown roles fall back to `subagent`.
|
|
546
|
+
* - `params.model` always wins over the role default.
|
|
547
|
+
*
|
|
548
|
+
* Reasoning goes through `modelKwargs` (spread verbatim into the request
|
|
549
|
+
* body), never ChatOpenAI's top-level `reasoning` field: that field is
|
|
550
|
+
* dropped for OpenRouter-prefixed ids, and a top-level `reasoning.summary`
|
|
551
|
+
* silently reroutes the call to the Responses API.
|
|
552
|
+
*
|
|
553
|
+
* Nebius mirrors the Node provider's second branch: the fixed
|
|
554
|
+
* {@link NEBIUS_MODEL_MAP} (no `DEFAULT_MODEL` participation, no fallback
|
|
555
|
+
* array, no reasoning kwargs), `temperature` 0 for `guard` / 0.8 otherwise.
|
|
556
|
+
*/
|
|
557
|
+
export function createLlmAdapter(
|
|
558
|
+
env: LlmEnv,
|
|
559
|
+
logger: Logger = NOOP_LOGGER,
|
|
560
|
+
): OpenRouterLlmAdapter {
|
|
561
|
+
if (env.LLM_PROVIDER === 'nebius') return createNebiusAdapter(env, logger);
|
|
562
|
+
const defaultModelId = getDefaultModelId(env);
|
|
563
|
+
const mainEffort = env.MAIN_REASONING_EFFORT ?? 'medium';
|
|
564
|
+
const headers: Record<string, string> = {
|
|
565
|
+
'HTTP-Referer': 'oracle-app.com',
|
|
566
|
+
'X-Title': env.ORACLE_NAME ?? 'Oracle App',
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const modelForRole = (role: ModelRole): string => {
|
|
570
|
+
if (role === 'main') return defaultModelId;
|
|
571
|
+
return (
|
|
572
|
+
OPENROUTER_MODEL_MAP[role as ProviderModelRole] ??
|
|
573
|
+
OPENROUTER_MODEL_MAP.subagent
|
|
574
|
+
);
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const get = (role: ModelRole, params?: ChatOpenAIFields): BaseChatModel => {
|
|
578
|
+
const {
|
|
579
|
+
model: paramsModel,
|
|
580
|
+
reasoning: paramsReasoning,
|
|
581
|
+
modelKwargs: paramsModelKwargs,
|
|
582
|
+
configuration: paramsConfiguration,
|
|
583
|
+
...rest
|
|
584
|
+
} = params ?? {};
|
|
585
|
+
const model =
|
|
586
|
+
typeof paramsModel === 'string' && paramsModel.length > 0
|
|
587
|
+
? paramsModel
|
|
588
|
+
: modelForRole(role);
|
|
589
|
+
|
|
590
|
+
const fallbackKwargs: Record<string, unknown> =
|
|
591
|
+
role === 'main'
|
|
592
|
+
? {
|
|
593
|
+
models: [...OPENROUTER_MAIN_FALLBACKS],
|
|
594
|
+
provider: { sort: 'latency' },
|
|
595
|
+
}
|
|
596
|
+
: {};
|
|
597
|
+
|
|
598
|
+
const reasoningKwargs = {
|
|
599
|
+
effort: role === 'main' ? mainEffort : 'medium',
|
|
600
|
+
summary: 'auto',
|
|
601
|
+
...asRecord(paramsReasoning),
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
logger.debug?.(
|
|
605
|
+
`[llm] creating model — role=${String(role)}, model=${model}, reasoning=${JSON.stringify(reasoningKwargs)}`,
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
const configuration = asRecord(paramsConfiguration);
|
|
609
|
+
const overrideHeaders: Record<string, string> = {};
|
|
610
|
+
for (const [key, value] of Object.entries(
|
|
611
|
+
asRecord(configuration.defaultHeaders),
|
|
612
|
+
)) {
|
|
613
|
+
if (typeof value === 'string') overrideHeaders[key] = value;
|
|
614
|
+
}
|
|
615
|
+
return new ChatOpenAI({
|
|
616
|
+
temperature: 0.8,
|
|
617
|
+
maxRetries: 2,
|
|
618
|
+
apiKey: env.OPEN_ROUTER_API_KEY,
|
|
619
|
+
...rest,
|
|
620
|
+
model,
|
|
621
|
+
__includeRawResponse: true,
|
|
622
|
+
configuration: {
|
|
623
|
+
baseURL: OPENROUTER_BASE_URL,
|
|
624
|
+
...configuration,
|
|
625
|
+
defaultHeaders: { ...headers, ...overrideHeaders },
|
|
626
|
+
},
|
|
627
|
+
modelKwargs: {
|
|
628
|
+
require_parameters: true,
|
|
629
|
+
include_reasoning: true,
|
|
630
|
+
...fallbackKwargs,
|
|
631
|
+
reasoning: reasoningKwargs,
|
|
632
|
+
...asRecord(paramsModelKwargs),
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
get,
|
|
639
|
+
modelForRole,
|
|
640
|
+
defaultModelId,
|
|
641
|
+
providerConfig: {
|
|
642
|
+
baseURL: OPENROUTER_BASE_URL,
|
|
643
|
+
apiKey: env.OPEN_ROUTER_API_KEY,
|
|
644
|
+
headers,
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Nebius Token Factory adapter — the Node provider's `nebius` branch:
|
|
651
|
+
* a plain OpenAI-compatible endpoint, the fixed role map, `temperature` 0
|
|
652
|
+
* for the `guard` role and 0.8 otherwise (callers override via `params`).
|
|
653
|
+
* No OpenRouter wire extras (`require_parameters`, fallback `models`,
|
|
654
|
+
* `reasoning` kwargs) — those break non-OpenRouter APIs.
|
|
655
|
+
*/
|
|
656
|
+
function createNebiusAdapter(
|
|
657
|
+
env: LlmEnv,
|
|
658
|
+
logger: Logger,
|
|
659
|
+
): OpenRouterLlmAdapter {
|
|
660
|
+
const apiKey = env.NEBIUS_API_KEY ?? '';
|
|
661
|
+
if (!apiKey) {
|
|
662
|
+
logger.warn(
|
|
663
|
+
'[llm] LLM_PROVIDER=nebius but NEBIUS_API_KEY is not set — model calls will fail',
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const modelForRole = (role: ModelRole): string =>
|
|
668
|
+
NEBIUS_MODEL_MAP[role as ProviderModelRole] ?? NEBIUS_MODEL_MAP.subagent;
|
|
669
|
+
|
|
670
|
+
const get = (role: ModelRole, params?: ChatOpenAIFields): BaseChatModel => {
|
|
671
|
+
const {
|
|
672
|
+
model: paramsModel,
|
|
673
|
+
configuration: paramsConfiguration,
|
|
674
|
+
...rest
|
|
675
|
+
} = params ?? {};
|
|
676
|
+
const model =
|
|
677
|
+
typeof paramsModel === 'string' && paramsModel.length > 0
|
|
678
|
+
? paramsModel
|
|
679
|
+
: modelForRole(role);
|
|
680
|
+
logger.debug?.(
|
|
681
|
+
`[llm] creating model — provider=nebius, role=${String(role)}, model=${model}`,
|
|
682
|
+
);
|
|
683
|
+
return new ChatOpenAI({
|
|
684
|
+
// Cold for classification (guard), platform default otherwise.
|
|
685
|
+
temperature: role === 'guard' ? 0 : 0.8,
|
|
686
|
+
maxRetries: 2,
|
|
687
|
+
apiKey,
|
|
688
|
+
__includeRawResponse: true,
|
|
689
|
+
...rest,
|
|
690
|
+
model,
|
|
691
|
+
configuration: {
|
|
692
|
+
baseURL: NEBIUS_BASE_URL,
|
|
693
|
+
...asRecord(paramsConfiguration),
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
return {
|
|
699
|
+
get,
|
|
700
|
+
modelForRole,
|
|
701
|
+
defaultModelId: NEBIUS_MODEL_MAP.main,
|
|
702
|
+
providerConfig: { baseURL: NEBIUS_BASE_URL, apiKey, headers: {} },
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ── LangSmith tracing ───────────────────────────────────────────────────────
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Per-turn LangSmith tracing decision — the Workers port of the Node
|
|
710
|
+
* runtime's `modules/messages/langsmith-tracing.ts`.
|
|
711
|
+
*
|
|
712
|
+
* One deliberate difference: on Node, `LANGSMITH_TRACING=true` relies on
|
|
713
|
+
* LangChain's env-driven auto-attached tracer. Workers has no `process.env`,
|
|
714
|
+
* so the callback manager never auto-attaches anything — here BOTH modes
|
|
715
|
+
* attach an explicit `LangChainTracer` whose `Client` is constructed from the
|
|
716
|
+
* Worker env (`LANGSMITH_API_KEY` / `LANGSMITH_ENDPOINT`) and memoised per
|
|
717
|
+
* isolate, so every tracer shares one batched upload queue:
|
|
718
|
+
*
|
|
719
|
+
* - **Global** — `LANGSMITH_TRACING === 'true'`: every turn is traced.
|
|
720
|
+
* - **Selective** — `LANGSMITH_TRACED_DIDS` allowlist (`*` = everyone):
|
|
721
|
+
* only turns whose UCAN-authenticated user DID matches are traced.
|
|
722
|
+
*
|
|
723
|
+
* Fail-closed: no API key ⇒ no tracer, nothing leaves the isolate. The
|
|
724
|
+
* `metadata` is returned unconditionally — attached to the run config, it is
|
|
725
|
+
* inert without a tracer and filterable per user in LangSmith with one.
|
|
726
|
+
*/
|
|
727
|
+
export interface LangsmithTracingEnv {
|
|
728
|
+
/** Raw `LANGSMITH_TRACING`. `'true'` (LangChain's exact check) = trace everyone. */
|
|
729
|
+
tracing?: string;
|
|
730
|
+
/** Raw `LANGSMITH_API_KEY`. Nothing is traced without it. */
|
|
731
|
+
apiKey?: string;
|
|
732
|
+
/** Raw `LANGSMITH_PROJECT`. Forwarded to the tracer when set. */
|
|
733
|
+
project?: string;
|
|
734
|
+
/** Raw `LANGSMITH_ENDPOINT` (self-hosted / EU). */
|
|
735
|
+
endpoint?: string;
|
|
736
|
+
/** Raw `LANGSMITH_TRACED_DIDS` comma-separated allowlist (`*` = everyone). */
|
|
737
|
+
tracedDids?: string;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** Narrow the raw Worker `env` bindings to the tracing env slice. */
|
|
741
|
+
export function langsmithEnvFromWorkerEnv(
|
|
742
|
+
env: Record<string, unknown>,
|
|
743
|
+
): LangsmithTracingEnv {
|
|
744
|
+
const str = (key: string): string | undefined => {
|
|
745
|
+
const value = env[key];
|
|
746
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
747
|
+
};
|
|
748
|
+
return {
|
|
749
|
+
tracing: str('LANGSMITH_TRACING'),
|
|
750
|
+
apiKey: str('LANGSMITH_API_KEY'),
|
|
751
|
+
project: str('LANGSMITH_PROJECT'),
|
|
752
|
+
endpoint: str('LANGSMITH_ENDPOINT'),
|
|
753
|
+
tracedDids: str('LANGSMITH_TRACED_DIDS'),
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export interface ResolveLangsmithTracingArgs {
|
|
758
|
+
/** The turn's user DID — from the authenticated payload, never client-set metadata. */
|
|
759
|
+
userDid: string;
|
|
760
|
+
/** Ingress surface, so latency can be sliced per client in LangSmith. */
|
|
761
|
+
client: 'portal' | 'matrix' | 'slack';
|
|
762
|
+
env: LangsmithTracingEnv;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export interface LangsmithTracingDecision {
|
|
766
|
+
/**
|
|
767
|
+
* Attached to the run config unconditionally — LangGraph forwards it to
|
|
768
|
+
* the tracer when one is attached, and it is inert otherwise.
|
|
769
|
+
*/
|
|
770
|
+
metadata: Record<string, string | number>;
|
|
771
|
+
/** Present only when this turn should be traced. */
|
|
772
|
+
callbacks?: [LangChainTracer];
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* One `langsmith` Client per (apiKey, endpoint) per isolate. Tracer instances
|
|
777
|
+
* are per-turn and cheap; the client owns the batched upload queue, so
|
|
778
|
+
* sharing it is what keeps tracing off the hot path.
|
|
779
|
+
*/
|
|
780
|
+
const langsmithClients = new Map<string, Client>();
|
|
781
|
+
|
|
782
|
+
function langsmithClientFor(apiKey: string, endpoint?: string): Client {
|
|
783
|
+
const key = `${apiKey}\n${endpoint ?? ''}`;
|
|
784
|
+
let client = langsmithClients.get(key);
|
|
785
|
+
if (!client) {
|
|
786
|
+
client = new Client({ apiKey, ...(endpoint && { apiUrl: endpoint }) });
|
|
787
|
+
langsmithClients.set(key, client);
|
|
788
|
+
}
|
|
789
|
+
return client;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** Single-slot memo for the parsed allowlist (raw string is deploy-stable). */
|
|
793
|
+
let tracedDidsMemoRaw: string | undefined;
|
|
794
|
+
let tracedDidsMemoSet: ReadonlySet<string> = new Set();
|
|
795
|
+
|
|
796
|
+
function parseTracedDids(raw: string | undefined): ReadonlySet<string> {
|
|
797
|
+
if (raw === tracedDidsMemoRaw) return tracedDidsMemoSet;
|
|
798
|
+
tracedDidsMemoRaw = raw;
|
|
799
|
+
tracedDidsMemoSet = new Set(
|
|
800
|
+
(raw ?? '')
|
|
801
|
+
.split(',')
|
|
802
|
+
.map((entry) => entry.trim())
|
|
803
|
+
.filter((entry) => entry.length > 0),
|
|
804
|
+
);
|
|
805
|
+
return tracedDidsMemoSet;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export function resolveLangsmithTracing(
|
|
809
|
+
args: ResolveLangsmithTracingArgs,
|
|
810
|
+
): LangsmithTracingDecision {
|
|
811
|
+
const { userDid, client, env } = args;
|
|
812
|
+
|
|
813
|
+
// `user_id` duplicates the DID under the key LangSmith examples use for
|
|
814
|
+
// user-scoped filtering; `user_did` is the self-describing key.
|
|
815
|
+
const metadata: Record<string, string | number> = {
|
|
816
|
+
user_did: userDid,
|
|
817
|
+
user_id: userDid,
|
|
818
|
+
client,
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
// Fail-closed: a tracer that cannot authenticate is never attached.
|
|
822
|
+
if (!env.apiKey) {
|
|
823
|
+
return { metadata };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const globalOn = env.tracing?.trim() === 'true';
|
|
827
|
+
if (!globalOn) {
|
|
828
|
+
const allowlist = parseTracedDids(env.tracedDids);
|
|
829
|
+
const traced = allowlist.has('*') || allowlist.has(userDid);
|
|
830
|
+
if (!traced) return { metadata };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return {
|
|
834
|
+
metadata,
|
|
835
|
+
callbacks: [
|
|
836
|
+
new LangChainTracer({
|
|
837
|
+
client: langsmithClientFor(env.apiKey, env.endpoint),
|
|
838
|
+
...(env.project && { projectName: env.project }),
|
|
839
|
+
}),
|
|
840
|
+
],
|
|
841
|
+
};
|
|
842
|
+
}
|