@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.
Files changed (215) hide show
  1. package/README.md +110 -0
  2. package/package.json +77 -0
  3. package/src/attachments/classify.ts +114 -0
  4. package/src/attachments/content-blocks.ts +51 -0
  5. package/src/attachments/download.ts +235 -0
  6. package/src/attachments/extract.ts +241 -0
  7. package/src/attachments/index.ts +84 -0
  8. package/src/attachments/magic.ts +133 -0
  9. package/src/attachments/pipeline.ts +409 -0
  10. package/src/attachments/retention.ts +205 -0
  11. package/src/attachments/route.ts +41 -0
  12. package/src/attachments/sandbox-archive.ts +90 -0
  13. package/src/attachments/sanitize.ts +35 -0
  14. package/src/attachments/types.ts +86 -0
  15. package/src/attachments/view.ts +107 -0
  16. package/src/core/env.ts +200 -0
  17. package/src/core/index.ts +513 -0
  18. package/src/core/llm.ts +842 -0
  19. package/src/core/main-agent-types.ts +123 -0
  20. package/src/core/main-agent.ts +399 -0
  21. package/src/core/manifest.ts +365 -0
  22. package/src/core/meta-tools.ts +232 -0
  23. package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
  24. package/src/core/middlewares/capability-gate.ts +76 -0
  25. package/src/core/middlewares/index.ts +18 -0
  26. package/src/core/middlewares/page-context.ts +98 -0
  27. package/src/core/middlewares/safety-guardrail.ts +117 -0
  28. package/src/core/middlewares/summarization.ts +99 -0
  29. package/src/core/middlewares/tool-repetition-guard.ts +125 -0
  30. package/src/core/middlewares/tool-validation.ts +83 -0
  31. package/src/core/middlewares/work-status.ts +57 -0
  32. package/src/core/openrouter-pricing.ts +124 -0
  33. package/src/core/plugin-loader.ts +192 -0
  34. package/src/core/plugins/index.ts +6 -0
  35. package/src/core/plugins/skills/index.ts +8 -0
  36. package/src/core/plugins/skills/skills-tools.ts +356 -0
  37. package/src/core/plugins/skills/skills.plugin.ts +113 -0
  38. package/src/core/plugins/weather/index.ts +8 -0
  39. package/src/core/plugins/weather/weather-client.ts +153 -0
  40. package/src/core/plugins/weather/weather-middleware.ts +27 -0
  41. package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
  42. package/src/core/plugins/weather/weather-tools.ts +103 -0
  43. package/src/core/plugins/weather/weather.plugin.ts +227 -0
  44. package/src/core/prompt-composer.ts +469 -0
  45. package/src/core/registries.ts +642 -0
  46. package/src/core/runtime-context.ts +619 -0
  47. package/src/core/state.ts +108 -0
  48. package/src/core/sub-agent-fallback.ts +187 -0
  49. package/src/core/subagent-as-tool.ts +322 -0
  50. package/src/core/template.ts +245 -0
  51. package/src/core/utils.ts +246 -0
  52. package/src/core/wrap-plugin-tool.ts +100 -0
  53. package/src/do/ambient.ts +266 -0
  54. package/src/do/contracts.ts +562 -0
  55. package/src/do/debug-timers.ts +120 -0
  56. package/src/do/fresh-stub.ts +32 -0
  57. package/src/do/gateway-retry.ts +73 -0
  58. package/src/do/secrets-adapter.ts +32 -0
  59. package/src/do/size-alerts.ts +76 -0
  60. package/src/do/sse-stream.ts +472 -0
  61. package/src/do/transcript.ts +192 -0
  62. package/src/do/turn-body.ts +84 -0
  63. package/src/do/turn-metadata.ts +146 -0
  64. package/src/do/ucan-service.ts +339 -0
  65. package/src/do/user-oracle-do.ts +3035 -0
  66. package/src/gateway-worker.ts +82 -0
  67. package/src/index.ts +237 -0
  68. package/src/llm/byo-adapter.ts +59 -0
  69. package/src/llm/byo-catalog.ts +520 -0
  70. package/src/llm/byo-client.ts +280 -0
  71. package/src/llm/byo-routes.ts +255 -0
  72. package/src/llm/byo-service.ts +836 -0
  73. package/src/llm/chatgpt-oauth.ts +430 -0
  74. package/src/llm/provider-error.ts +283 -0
  75. package/src/matrix/gateway-do.ts +918 -0
  76. package/src/matrix/ingest.ts +209 -0
  77. package/src/matrix/replay-format.ts +34 -0
  78. package/src/matrix/reply-chain.ts +109 -0
  79. package/src/matrix/room-state-codec.ts +115 -0
  80. package/src/matrix/user-homeserver.ts +106 -0
  81. package/src/matrix/work-status.ts +294 -0
  82. package/src/memory/session-history-indexer.ts +328 -0
  83. package/src/owner-store/ixo-vfs-store.ts +570 -0
  84. package/src/owner-store/matrix-media-store.ts +136 -0
  85. package/src/owner-store/migrating-store.ts +185 -0
  86. package/src/owner-store/owner-copy-errors.ts +175 -0
  87. package/src/owner-store/retry.ts +45 -0
  88. package/src/owner-store/tus-upload.ts +333 -0
  89. package/src/owner-store/types.ts +0 -0
  90. package/src/plugin-api/define-plugin.ts +45 -0
  91. package/src/plugin-api/index.ts +56 -0
  92. package/src/plugin-api/oracle-plugin.ts +145 -0
  93. package/src/plugin-api/tool-helper.ts +82 -0
  94. package/src/plugin-api/types.ts +816 -0
  95. package/src/plugin-api/ucan-errors.ts +16 -0
  96. package/src/plugins/agui/agui-agent.ts +115 -0
  97. package/src/plugins/agui/agui.plugin.ts +148 -0
  98. package/src/plugins/agui/index.ts +2 -0
  99. package/src/plugins/attachments/attachments.plugin.ts +54 -0
  100. package/src/plugins/attachments/index.ts +6 -0
  101. package/src/plugins/attachments/view-attachment-tool.ts +91 -0
  102. package/src/plugins/base64.ts +26 -0
  103. package/src/plugins/composio/composio-tools.ts +365 -0
  104. package/src/plugins/composio/composio-ucan.ts +43 -0
  105. package/src/plugins/composio/composio.plugin.ts +214 -0
  106. package/src/plugins/composio/index.ts +15 -0
  107. package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
  108. package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
  109. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
  110. package/src/plugins/domain-indexer/index.ts +3 -0
  111. package/src/plugins/editor/blocknote-bridge.ts +120 -0
  112. package/src/plugins/editor/content-session.ts +331 -0
  113. package/src/plugins/editor/content-tools.ts +686 -0
  114. package/src/plugins/editor/document-model.ts +0 -0
  115. package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
  116. package/src/plugins/editor/editor-agent.ts +34 -0
  117. package/src/plugins/editor/editor-config.ts +64 -0
  118. package/src/plugins/editor/editor-mx.ts +128 -0
  119. package/src/plugins/editor/editor.plugin.ts +143 -0
  120. package/src/plugins/editor/failures.ts +154 -0
  121. package/src/plugins/editor/index.ts +31 -0
  122. package/src/plugins/editor/jsdom-shim.ts +55 -0
  123. package/src/plugins/editor/prompts.ts +59 -0
  124. package/src/plugins/editor/prop-policy.ts +147 -0
  125. package/src/plugins/editor/provider.ts +386 -0
  126. package/src/plugins/editor/room-membership.ts +95 -0
  127. package/src/plugins/editor/standalone-editor-tool.ts +200 -0
  128. package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
  129. package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
  130. package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
  131. package/src/plugins/firecrawl/index.ts +15 -0
  132. package/src/plugins/flows/action-metadata.ts +3505 -0
  133. package/src/plugins/flows/actions.ts +190 -0
  134. package/src/plugins/flows/edit.ts +314 -0
  135. package/src/plugins/flows/errors.ts +38 -0
  136. package/src/plugins/flows/explain.ts +43 -0
  137. package/src/plugins/flows/flow-doc.ts +111 -0
  138. package/src/plugins/flows/flows.plugin.ts +129 -0
  139. package/src/plugins/flows/forms.ts +330 -0
  140. package/src/plugins/flows/index.ts +2 -0
  141. package/src/plugins/flows/linkage.ts +147 -0
  142. package/src/plugins/flows/port-types.ts +53 -0
  143. package/src/plugins/flows/prompts.ts +65 -0
  144. package/src/plugins/flows/read.ts +392 -0
  145. package/src/plugins/flows/references.ts +66 -0
  146. package/src/plugins/flows/test-support.ts +165 -0
  147. package/src/plugins/flows/tools/authoring.ts +516 -0
  148. package/src/plugins/flows/tools/discovery.ts +119 -0
  149. package/src/plugins/flows/tools/forms.ts +142 -0
  150. package/src/plugins/flows/tools/inspect.ts +165 -0
  151. package/src/plugins/flows/tools/linkage.ts +117 -0
  152. package/src/plugins/flows/tools/settings.ts +184 -0
  153. package/src/plugins/flows/translator.ts +328 -0
  154. package/src/plugins/flows/types.ts +210 -0
  155. package/src/plugins/flows/ydoc-helpers.ts +543 -0
  156. package/src/plugins/index.ts +70 -0
  157. package/src/plugins/mcp-call-timeout.ts +38 -0
  158. package/src/plugins/mcp-tool-adapter.ts +87 -0
  159. package/src/plugins/memory/index.ts +17 -0
  160. package/src/plugins/memory/memory-tools.ts +463 -0
  161. package/src/plugins/memory/memory-ucan.ts +51 -0
  162. package/src/plugins/memory/memory.plugin.ts +122 -0
  163. package/src/plugins/memory/types.ts +147 -0
  164. package/src/plugins/portal/action-log.ts +34 -0
  165. package/src/plugins/portal/index.ts +2 -0
  166. package/src/plugins/portal/portal.plugin.ts +203 -0
  167. package/src/plugins/sandbox/index.ts +34 -0
  168. package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
  169. package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
  170. package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
  171. package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
  172. package/src/plugins/tasks/index.ts +17 -0
  173. package/src/plugins/tasks/manifest.ts +81 -0
  174. package/src/plugins/tasks/middleware.ts +219 -0
  175. package/src/plugins/tasks/tasks-tools.ts +414 -0
  176. package/src/plugins/tasks/tasks.plugin.ts +74 -0
  177. package/src/plugins/ucan-failure.ts +133 -0
  178. package/src/plugins/user-preferences/index.ts +22 -0
  179. package/src/plugins/user-preferences/schema.ts +31 -0
  180. package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
  181. package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
  182. package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
  183. package/src/plugins/vfs/index.ts +40 -0
  184. package/src/plugins/vfs/vfs-auth.ts +56 -0
  185. package/src/plugins/vfs/vfs-client.ts +655 -0
  186. package/src/plugins/vfs/vfs-content.ts +181 -0
  187. package/src/plugins/vfs/vfs-errors.ts +163 -0
  188. package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
  189. package/src/plugins/vfs/vfs-tools.ts +635 -0
  190. package/src/plugins/vfs/vfs.plugin.ts +201 -0
  191. package/src/realtime/frontend-call-registry.ts +163 -0
  192. package/src/realtime/realtime-endpoint.ts +619 -0
  193. package/src/realtime/session-socket-hub.ts +249 -0
  194. package/src/realtime/socket-io-codec.ts +187 -0
  195. package/src/secrets/jwe.ts +74 -0
  196. package/src/secrets/pin-cipher.ts +43 -0
  197. package/src/secrets/secrets-service.ts +319 -0
  198. package/src/shell/app.ts +564 -0
  199. package/src/shell/auth.ts +290 -0
  200. package/src/sqlite/blob-codec.ts +100 -0
  201. package/src/sqlite/blob-compactor.ts +146 -0
  202. package/src/sqlite/cache-config.ts +45 -0
  203. package/src/sqlite/database.ts +643 -0
  204. package/src/sqlite/do-vfs.ts +1485 -0
  205. package/src/sqlite/index.ts +61 -0
  206. package/src/sqlite/serialization.ts +370 -0
  207. package/src/sqlite/sessions-store.ts +311 -0
  208. package/src/sqlite/sqlite-saver.ts +919 -0
  209. package/src/sqlite/vacuum-policy.ts +77 -0
  210. package/src/sqlite/wa-sqlite-loader.ts +123 -0
  211. package/src/sqlite/wasm.d.ts +10 -0
  212. package/src/tasks/schedule.ts +174 -0
  213. package/src/tasks/scheduler.ts +753 -0
  214. package/src/tasks/spec.ts +125 -0
  215. package/src/tasks/store.ts +351 -0
@@ -0,0 +1,520 @@
1
+ /**
2
+ * Bring-your-own-credential (BYO) provider and model catalog — the Workers
3
+ * port of the Node runtime's `llm/byo-catalog.ts`.
4
+ *
5
+ * When `BYO_LLM_ENABLED` is on, a user can connect their own ChatGPT
6
+ * subscription (OAuth) or per-provider API keys and run their chat turns on
7
+ * their own account instead of the platform's OpenRouter key. This file is
8
+ * the single source of truth for:
9
+ *
10
+ * - which providers are supported and how each authenticates,
11
+ * - the reserved room-secret name each credential is stored under,
12
+ * - the `byo:` model-id namespace and its parsing,
13
+ * - the curated per-provider model list shown in the picker, and
14
+ * - the role→model translation used so sub-agents, vision, guards etc. run
15
+ * on the user's provider too (embedding always stays platform-side).
16
+ *
17
+ * Everything here is pure data + pure functions; credential resolution and
18
+ * client construction live in `byo-service.ts` / `byo-client.ts`. Wire shapes
19
+ * (secret names, `byo:` ids) are identical to the Node runtime, so a user's
20
+ * connected credentials survive the runtime swap.
21
+ */
22
+
23
+ import {
24
+ TIER_DISPLAY,
25
+ type ModelListItem,
26
+ type ModelTier,
27
+ type ProviderModelRole,
28
+ } from '../core/llm';
29
+
30
+ export type ByoProvider =
31
+ | 'chatgpt'
32
+ | 'openai'
33
+ | 'anthropic'
34
+ | 'gemini'
35
+ | 'deepseek';
36
+
37
+ export const BYO_PROVIDERS: readonly ByoProvider[] = [
38
+ 'chatgpt',
39
+ 'openai',
40
+ 'anthropic',
41
+ 'gemini',
42
+ 'deepseek',
43
+ ];
44
+
45
+ export function isByoProvider(value: string): value is ByoProvider {
46
+ return (BYO_PROVIDERS as readonly string[]).includes(value);
47
+ }
48
+
49
+ /** How a provider's credential is obtained on the connect surface. */
50
+ export type ByoAuthType = 'oauth' | 'api-key';
51
+
52
+ export interface ByoProviderInfo {
53
+ provider: ByoProvider;
54
+ /** Display name for the connect UI. */
55
+ label: string;
56
+ authType: ByoAuthType;
57
+ /** Picker badge for models running on this credential. */
58
+ badge: string;
59
+ }
60
+
61
+ export const BYO_PROVIDER_INFO: Record<ByoProvider, ByoProviderInfo> = {
62
+ chatgpt: {
63
+ provider: 'chatgpt',
64
+ label: 'ChatGPT (subscription)',
65
+ authType: 'oauth',
66
+ badge: 'Your subscription',
67
+ },
68
+ openai: {
69
+ provider: 'openai',
70
+ label: 'OpenAI API',
71
+ authType: 'api-key',
72
+ badge: 'Your API key',
73
+ },
74
+ anthropic: {
75
+ provider: 'anthropic',
76
+ label: 'Anthropic API',
77
+ authType: 'api-key',
78
+ badge: 'Your API key',
79
+ },
80
+ gemini: {
81
+ provider: 'gemini',
82
+ label: 'Google Gemini API',
83
+ authType: 'api-key',
84
+ badge: 'Your API key',
85
+ },
86
+ deepseek: {
87
+ provider: 'deepseek',
88
+ label: 'DeepSeek API',
89
+ authType: 'api-key',
90
+ badge: 'Your API key',
91
+ },
92
+ };
93
+
94
+ /**
95
+ * Reserved room-secret names, one per provider. The portal writes API keys
96
+ * under these names via the existing agent-secrets flow (JWE-encrypted to the
97
+ * oracle's on-chain P-256 key); the ChatGPT OAuth blob is written server-side
98
+ * after the token exchange. Stored in the canonical user↔oracle room, so the
99
+ * credential is account-level for this oracle, not per-session.
100
+ */
101
+ export const BYO_SECRET_NAMES: Record<ByoProvider, string> = {
102
+ chatgpt: 'BYO_LLM_CHATGPT_OAUTH',
103
+ openai: 'BYO_LLM_OPENAI_API_KEY',
104
+ anthropic: 'BYO_LLM_ANTHROPIC_API_KEY',
105
+ gemini: 'BYO_LLM_GEMINI_API_KEY',
106
+ deepseek: 'BYO_LLM_DEEPSEEK_API_KEY',
107
+ };
108
+
109
+ const SECRET_NAME_TO_PROVIDER = new Map<string, ByoProvider>(
110
+ (Object.entries(BYO_SECRET_NAMES) as Array<[ByoProvider, string]>).map(
111
+ ([provider, name]) => [name, provider],
112
+ ),
113
+ );
114
+
115
+ /** Reverse lookup: room-secret name → provider (undefined for non-BYO names). */
116
+ export function providerForSecretName(name: string): ByoProvider | undefined {
117
+ return SECRET_NAME_TO_PROVIDER.get(name);
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Credentials
122
+ // ---------------------------------------------------------------------------
123
+
124
+ /** Parsed ChatGPT OAuth blob stored under `BYO_LLM_CHATGPT_OAUTH`. */
125
+ export interface ChatGptOAuthTokens {
126
+ accessToken: string;
127
+ refreshToken: string;
128
+ /** `chatgpt_account_id` claim — sent as a header on backend calls. */
129
+ accountId: string;
130
+ /** Epoch ms when `accessToken` expires. */
131
+ expiresAt: number;
132
+ }
133
+
134
+ export type ByoCredential =
135
+ | { provider: 'chatgpt'; oauth: ChatGptOAuthTokens }
136
+ | {
137
+ provider: Exclude<ByoProvider, 'chatgpt'>;
138
+ apiKey: string;
139
+ };
140
+
141
+ /**
142
+ * Parse the stored ChatGPT OAuth JSON. Returns `null` on any malformed value
143
+ * so a corrupted secret degrades to "not connected" instead of throwing on
144
+ * the chat hot path.
145
+ */
146
+ export function parseChatGptOAuthTokens(
147
+ raw: string,
148
+ ): ChatGptOAuthTokens | null {
149
+ try {
150
+ const parsed: unknown = JSON.parse(raw);
151
+ if (!parsed || typeof parsed !== 'object') return null;
152
+ const o = parsed as Record<string, unknown>;
153
+ if (
154
+ typeof o.accessToken === 'string' &&
155
+ o.accessToken.length > 0 &&
156
+ typeof o.refreshToken === 'string' &&
157
+ typeof o.accountId === 'string' &&
158
+ typeof o.expiresAt === 'number'
159
+ ) {
160
+ return {
161
+ accessToken: o.accessToken,
162
+ refreshToken: o.refreshToken,
163
+ accountId: o.accountId,
164
+ expiresAt: o.expiresAt,
165
+ };
166
+ }
167
+ return null;
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Model-id namespace
175
+ // ---------------------------------------------------------------------------
176
+
177
+ /**
178
+ * BYO model ids are namespaced `byo:<provider>/<provider-native-id>` so they
179
+ * can never collide with OpenRouter slugs in the platform catalog, and so the
180
+ * allow-list gate can route them to the BYO validation path.
181
+ */
182
+ export const BYO_MODEL_PREFIX = 'byo:';
183
+
184
+ export function isByoModelId(id: string | undefined | null): id is string {
185
+ return typeof id === 'string' && id.startsWith(BYO_MODEL_PREFIX);
186
+ }
187
+
188
+ export function toByoModelId(provider: ByoProvider, modelId: string): string {
189
+ return `${BYO_MODEL_PREFIX}${provider}/${modelId}`;
190
+ }
191
+
192
+ export interface ParsedByoModelId {
193
+ provider: ByoProvider;
194
+ /** The provider-native model id (what goes on the wire). */
195
+ modelId: string;
196
+ }
197
+
198
+ /**
199
+ * Parse and validate a `byo:` model id. Returns `null` for unknown providers
200
+ * or models outside the curated per-provider list — this is the BYO
201
+ * counterpart of `isAllowedModel`, so a client can't point a turn at an
202
+ * arbitrary model even on their own key.
203
+ */
204
+ export function parseByoModelId(
205
+ id: string | undefined | null,
206
+ ): ParsedByoModelId | null {
207
+ if (!isByoModelId(id)) return null;
208
+ const rest = id.slice(BYO_MODEL_PREFIX.length);
209
+ const slash = rest.indexOf('/');
210
+ if (slash <= 0) return null;
211
+ const provider = rest.slice(0, slash);
212
+ const modelId = rest.slice(slash + 1);
213
+ if (!isByoProvider(provider) || modelId.length === 0) return null;
214
+ const known = BYO_PROVIDER_MODELS[provider].some((m) => m.id === modelId);
215
+ return known ? { provider, modelId } : null;
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Curated per-provider models
220
+ // ---------------------------------------------------------------------------
221
+
222
+ export interface ByoModelEntry {
223
+ /** Provider-native model id (sent on the wire, no `byo:` prefix). */
224
+ id: string;
225
+ label: string;
226
+ tier: ModelTier;
227
+ blurb: string;
228
+ vision: boolean;
229
+ }
230
+
231
+ /**
232
+ * The models a connected user can pick per provider. Kept deliberately small,
233
+ * mirroring the platform catalog's curation stance. Ids are the providers'
234
+ * native API ids.
235
+ */
236
+ export const BYO_PROVIDER_MODELS: Record<
237
+ ByoProvider,
238
+ readonly ByoModelEntry[]
239
+ > = {
240
+ // Served by the ChatGPT backend on the user's subscription quota. Ids match
241
+ // the backend's live model catalog (the Codex client's models.json).
242
+ chatgpt: [
243
+ {
244
+ id: 'gpt-5.6-luna',
245
+ label: 'GPT-5.6 Luna',
246
+ tier: 'everyday',
247
+ blurb: 'Runs on your ChatGPT plan — fast everyday model.',
248
+ vision: true,
249
+ },
250
+ {
251
+ id: 'gpt-5.6-terra',
252
+ label: 'GPT-5.6 Terra',
253
+ tier: 'balanced',
254
+ blurb: 'Runs on your ChatGPT plan — the balanced flagship.',
255
+ vision: true,
256
+ },
257
+ {
258
+ id: 'gpt-5.6-sol',
259
+ label: 'GPT-5.6 Sol',
260
+ tier: 'top',
261
+ blurb: 'Runs on your ChatGPT plan — strongest for complex work.',
262
+ vision: true,
263
+ },
264
+ ],
265
+ openai: [
266
+ {
267
+ id: 'gpt-5.6-luna',
268
+ label: 'GPT-5.6 Luna',
269
+ tier: 'everyday',
270
+ blurb: 'Fast and low-cost, billed to your OpenAI account.',
271
+ vision: true,
272
+ },
273
+ {
274
+ id: 'gpt-5.6-terra',
275
+ label: 'GPT-5.6 Terra',
276
+ tier: 'balanced',
277
+ blurb: 'Smart all-rounder, billed to your OpenAI account.',
278
+ vision: true,
279
+ },
280
+ {
281
+ id: 'gpt-5.6-sol',
282
+ label: 'GPT-5.6 Sol',
283
+ tier: 'top',
284
+ blurb: "OpenAI's flagship, billed to your OpenAI account.",
285
+ vision: true,
286
+ },
287
+ ],
288
+ anthropic: [
289
+ {
290
+ id: 'claude-haiku-4-5',
291
+ label: 'Claude Haiku 4.5',
292
+ tier: 'everyday',
293
+ blurb: 'Fast and inexpensive on your Anthropic key.',
294
+ vision: true,
295
+ },
296
+ {
297
+ id: 'claude-sonnet-5',
298
+ label: 'Claude Sonnet 5',
299
+ tier: 'balanced',
300
+ blurb: 'Careful writing, reasoning and coding on your Anthropic key.',
301
+ vision: true,
302
+ },
303
+ {
304
+ id: 'claude-opus-5',
305
+ label: 'Claude Opus 5',
306
+ tier: 'top',
307
+ blurb: "Anthropic's most capable model on your Anthropic key.",
308
+ vision: true,
309
+ },
310
+ ],
311
+ gemini: [
312
+ {
313
+ id: 'gemini-3.1-flash-lite',
314
+ label: 'Gemini 3.1 Flash Lite',
315
+ tier: 'everyday',
316
+ blurb: 'Speedy and inexpensive on your Gemini key.',
317
+ vision: true,
318
+ },
319
+ {
320
+ id: 'gemini-3.6-flash',
321
+ label: 'Gemini 3.6 Flash',
322
+ tier: 'balanced',
323
+ blurb: 'Capable multimodal model on your Gemini key.',
324
+ vision: true,
325
+ },
326
+ {
327
+ id: 'gemini-3.1-pro',
328
+ label: 'Gemini 3.1 Pro',
329
+ tier: 'top',
330
+ blurb: "Google's most capable model on your Gemini key.",
331
+ vision: true,
332
+ },
333
+ ],
334
+ deepseek: [
335
+ {
336
+ id: 'deepseek-v4-flash',
337
+ label: 'DeepSeek V4 Flash',
338
+ tier: 'everyday',
339
+ blurb: 'Budget-friendly general chat on your DeepSeek key.',
340
+ vision: false,
341
+ },
342
+ {
343
+ id: 'deepseek-v4-pro',
344
+ label: 'DeepSeek V4 Pro',
345
+ tier: 'balanced',
346
+ blurb: 'Deep step-by-step reasoning on your DeepSeek key.',
347
+ vision: false,
348
+ },
349
+ ],
350
+ };
351
+
352
+ /** Default `main` model when a user connects a provider (picker preselect). */
353
+ export const BYO_DEFAULT_MODEL: Record<ByoProvider, string> = {
354
+ chatgpt: 'gpt-5.6-terra',
355
+ openai: 'gpt-5.6-terra',
356
+ anthropic: 'claude-sonnet-5',
357
+ gemini: 'gemini-3.6-flash',
358
+ deepseek: 'deepseek-v4-flash',
359
+ };
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // Role translation
363
+ // ---------------------------------------------------------------------------
364
+
365
+ /**
366
+ * Role→model translation per provider. On a BYO turn every model role the
367
+ * provider can serve runs on the user's credential — sub-agents, vision,
368
+ * guards, routing, titles — so the user's own account absorbs the whole turn.
369
+ * A role absent here falls back to the platform provider (deliberately:
370
+ * `embedding` everywhere — none of these providers is wired for the runtime's
371
+ * embedding path — and `vision` on DeepSeek, whose chat models are text-only).
372
+ */
373
+ export const BYO_ROLE_MODELS: Record<
374
+ ByoProvider,
375
+ Partial<Record<ProviderModelRole, string>>
376
+ > = {
377
+ chatgpt: {
378
+ skills: 'gpt-5.6-luna',
379
+ subagent: 'gpt-5.6-luna',
380
+ vision: 'gpt-5.6-luna',
381
+ guard: 'gpt-5.6-luna',
382
+ routing: 'gpt-5.6-luna',
383
+ 'session-title': 'gpt-5.6-luna',
384
+ custom_low: 'gpt-5.6-luna',
385
+ custom_medium: 'gpt-5.6-terra',
386
+ },
387
+ openai: {
388
+ skills: 'gpt-5.6-luna',
389
+ subagent: 'gpt-5.6-luna',
390
+ vision: 'gpt-5.6-luna',
391
+ guard: 'gpt-5.6-luna',
392
+ routing: 'gpt-5.6-luna',
393
+ 'session-title': 'gpt-5.6-luna',
394
+ custom_low: 'gpt-5.6-luna',
395
+ custom_medium: 'gpt-5.6-terra',
396
+ },
397
+ anthropic: {
398
+ skills: 'claude-haiku-4-5',
399
+ subagent: 'claude-haiku-4-5',
400
+ vision: 'claude-haiku-4-5',
401
+ guard: 'claude-haiku-4-5',
402
+ routing: 'claude-haiku-4-5',
403
+ 'session-title': 'claude-haiku-4-5',
404
+ custom_low: 'claude-haiku-4-5',
405
+ custom_medium: 'claude-sonnet-5',
406
+ },
407
+ gemini: {
408
+ skills: 'gemini-3.1-flash-lite',
409
+ subagent: 'gemini-3.1-flash-lite',
410
+ vision: 'gemini-3.1-flash-lite',
411
+ guard: 'gemini-3.1-flash-lite',
412
+ routing: 'gemini-3.1-flash-lite',
413
+ 'session-title': 'gemini-3.1-flash-lite',
414
+ custom_low: 'gemini-3.1-flash-lite',
415
+ custom_medium: 'gemini-3.6-flash',
416
+ },
417
+ deepseek: {
418
+ skills: 'deepseek-v4-flash',
419
+ subagent: 'deepseek-v4-flash',
420
+ guard: 'deepseek-v4-flash',
421
+ routing: 'deepseek-v4-flash',
422
+ 'session-title': 'deepseek-v4-flash',
423
+ custom_low: 'deepseek-v4-flash',
424
+ custom_medium: 'deepseek-v4-pro',
425
+ },
426
+ };
427
+
428
+ /**
429
+ * The full role vocabulary the provider maps cover. A KNOWN role absent from
430
+ * a provider's map is deliberately unserved (embedding everywhere, vision on
431
+ * DeepSeek) and must fall back to the platform adapter — while an UNKNOWN
432
+ * plugin-custom role falls back to the provider's cheap `subagent` model,
433
+ * mirroring the platform adapter's behaviour.
434
+ */
435
+ const KNOWN_MODEL_ROLES: ReadonlySet<string> = new Set([
436
+ 'main',
437
+ 'skills',
438
+ 'subagent',
439
+ 'vision',
440
+ 'guard',
441
+ 'routing',
442
+ 'session-title',
443
+ 'embedding',
444
+ 'custom_low',
445
+ 'custom_medium',
446
+ ]);
447
+
448
+ /**
449
+ * Resolve the provider-native model id for a role on a BYO turn.
450
+ * `main` uses the turn's selected model; other roles use the translation
451
+ * table. `null` → the role is not served by this provider and the caller
452
+ * must fall back to the platform adapter.
453
+ */
454
+ export function byoModelForRole(
455
+ provider: ByoProvider,
456
+ role: ProviderModelRole | string,
457
+ mainModelId: string,
458
+ ): string | null {
459
+ if (role === 'main') return mainModelId;
460
+ const map = BYO_ROLE_MODELS[provider];
461
+ const direct = map[role as ProviderModelRole];
462
+ if (direct) return direct;
463
+ if (KNOWN_MODEL_ROLES.has(role)) return null;
464
+ return map.subagent ?? null;
465
+ }
466
+
467
+ // ---------------------------------------------------------------------------
468
+ // Listing
469
+ // ---------------------------------------------------------------------------
470
+
471
+ /**
472
+ * Build picker entries for a user's connected providers, in the platform
473
+ * `ModelListItem` shape so the client SDK renders them without changes. Tier
474
+ * badge and cost label mirror the platform catalog so BYO rows read the same
475
+ * (the Workers catalog carries no pricing — the user pays their provider
476
+ * directly); the client marks BYO-ness itself from the `byo:` id namespace.
477
+ */
478
+ export function buildByoModelListing(
479
+ connected: readonly ByoProvider[],
480
+ ): ModelListItem[] {
481
+ const items: ModelListItem[] = [];
482
+ for (const provider of connected) {
483
+ for (const entry of BYO_PROVIDER_MODELS[provider]) {
484
+ items.push({
485
+ id: toByoModelId(provider, entry.id),
486
+ label: entry.label,
487
+ family: byoFamily(provider),
488
+ tier: entry.tier,
489
+ costLabel: TIER_DISPLAY[entry.tier].costLabel,
490
+ badge: TIER_DISPLAY[entry.tier].badge,
491
+ blurb: entry.blurb,
492
+ vision: entry.vision,
493
+ // The user pays their own provider; the platform charges nothing per
494
+ // token for a BYO model, so the listed price is zero.
495
+ pricing: {
496
+ inputPerMillion: 0,
497
+ outputPerMillion: 0,
498
+ currency: 'USD',
499
+ unit: 'per_million_tokens',
500
+ },
501
+ isDefault: false,
502
+ });
503
+ }
504
+ }
505
+ return items;
506
+ }
507
+
508
+ function byoFamily(provider: ByoProvider): ModelListItem['family'] {
509
+ switch (provider) {
510
+ case 'chatgpt':
511
+ case 'openai':
512
+ return 'openai';
513
+ case 'anthropic':
514
+ return 'anthropic';
515
+ case 'gemini':
516
+ return 'google';
517
+ case 'deepseek':
518
+ return 'deepseek';
519
+ }
520
+ }