@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
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat-model construction for BYO (bring-your-own-credential) turns — the
|
|
3
|
+
* Workers port of the Node runtime's `llm/byo-client.ts`. Given a resolved
|
|
4
|
+
* user credential and a provider-native model id, returns a LangChain chat
|
|
5
|
+
* model wired to the user's own account. Deliberately does NOT reuse the
|
|
6
|
+
* OpenRouter factory path — `require_parameters`, `models` fallbacks and the
|
|
7
|
+
* `reasoning` modelKwargs block are OpenRouter wire format and break direct
|
|
8
|
+
* provider APIs.
|
|
9
|
+
*
|
|
10
|
+
* One Workers-specific difference from Node: the `anthropic` branch speaks to
|
|
11
|
+
* Anthropic's official OpenAI SDK-compatibility endpoint
|
|
12
|
+
* (`https://api.anthropic.com/v1/`) through `ChatOpenAI` instead of
|
|
13
|
+
* `ChatAnthropic` — `@langchain/anthropic` is not part of this package's
|
|
14
|
+
* dependency set. Model ids and the user's API key are unchanged.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
18
|
+
import { ChatOpenAI } from '@langchain/openai';
|
|
19
|
+
import type { ChatOpenAIFields, Logger } from '../plugin-api/types';
|
|
20
|
+
import type { ProviderModelRole } from '../core/llm';
|
|
21
|
+
import type { ByoCredential } from './byo-catalog';
|
|
22
|
+
|
|
23
|
+
/** OpenAI-compatible base URLs for the API-key providers that need one. */
|
|
24
|
+
export const DEEPSEEK_BASE_URL = 'https://api.deepseek.com/v1';
|
|
25
|
+
export const GEMINI_OPENAI_COMPAT_BASE_URL =
|
|
26
|
+
'https://generativelanguage.googleapis.com/v1beta/openai/';
|
|
27
|
+
/** Anthropic's OpenAI SDK-compatibility endpoint (see module docblock). */
|
|
28
|
+
export const ANTHROPIC_OPENAI_COMPAT_BASE_URL = 'https://api.anthropic.com/v1/';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ChatGPT-backend wire constants — the endpoint the Codex client family uses
|
|
32
|
+
* to run models on a subscription. The base URL is joined with `/responses`
|
|
33
|
+
* by the OpenAI SDK when `useResponsesApi` is on.
|
|
34
|
+
*/
|
|
35
|
+
export const CHATGPT_BACKEND_BASE_URL = 'https://chatgpt.com/backend-api/codex';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Where ChatGPT-subscription requests go. `chatgpt.com` answers Cloudflare
|
|
39
|
+
* Workers egress with an HTML 403 before any authentication, so a deployment
|
|
40
|
+
* can point this lane at a transparent proxy on a non-Cloudflare host
|
|
41
|
+
* (`ixo-proxy-app`, one container per upstream) that forwards to the Codex
|
|
42
|
+
* backend byte for byte; the proxy's own gate is a shared secret in
|
|
43
|
+
* `X-Proxy-Auth`. Nothing else changes: the user's bearer token, the
|
|
44
|
+
* account header and the SSE stream pass through untouched. The OAuth
|
|
45
|
+
* device flow and token refresh (`auth.openai.com`) are reachable from
|
|
46
|
+
* Workers and never go through the proxy.
|
|
47
|
+
*/
|
|
48
|
+
export interface ChatGptBackendConfig {
|
|
49
|
+
/** Base URL standing in for `CHATGPT_BACKEND_BASE_URL`, no trailing slash. */
|
|
50
|
+
baseUrl: string;
|
|
51
|
+
/** Sent as `X-Proxy-Auth: Bearer <token>` when set. */
|
|
52
|
+
proxyAuthToken?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const DEFAULT_CHATGPT_BACKEND: ChatGptBackendConfig = {
|
|
56
|
+
baseUrl: CHATGPT_BACKEND_BASE_URL,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `BYO_CHATGPT_BACKEND_URL` (+ `BYO_CHATGPT_PROXY_AUTH_TOKEN`) → config.
|
|
61
|
+
* Unset or blank means the real backend. An invalid URL is a boot error,
|
|
62
|
+
* not a silent fallback to the real backend that would be blocked anyway.
|
|
63
|
+
*/
|
|
64
|
+
export function chatGptBackendFromEnv(env: {
|
|
65
|
+
BYO_CHATGPT_BACKEND_URL?: string;
|
|
66
|
+
BYO_CHATGPT_PROXY_AUTH_TOKEN?: string;
|
|
67
|
+
}): ChatGptBackendConfig {
|
|
68
|
+
const raw = env.BYO_CHATGPT_BACKEND_URL?.trim();
|
|
69
|
+
if (!raw) return DEFAULT_CHATGPT_BACKEND;
|
|
70
|
+
let url: URL;
|
|
71
|
+
try {
|
|
72
|
+
url = new URL(raw);
|
|
73
|
+
} catch {
|
|
74
|
+
throw new Error(`BYO_CHATGPT_BACKEND_URL is not a valid URL: ${raw}`);
|
|
75
|
+
}
|
|
76
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
77
|
+
throw new Error(`BYO_CHATGPT_BACKEND_URL must be http(s): ${raw}`);
|
|
78
|
+
const token = env.BYO_CHATGPT_PROXY_AUTH_TOKEN?.trim();
|
|
79
|
+
return {
|
|
80
|
+
baseUrl: raw.replace(/\/+$/, ''),
|
|
81
|
+
...(token ? { proxyAuthToken: token } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Headers the proxy gate needs, if any. */
|
|
86
|
+
export function chatGptBackendHeaders(
|
|
87
|
+
backend: ChatGptBackendConfig,
|
|
88
|
+
): Record<string, string> {
|
|
89
|
+
return backend.proxyAuthToken
|
|
90
|
+
? { 'X-Proxy-Auth': `Bearer ${backend.proxyAuthToken}` }
|
|
91
|
+
: {};
|
|
92
|
+
}
|
|
93
|
+
const CHATGPT_ORIGINATOR = 'codex_cli_rs';
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The ChatGPT backend reports request errors with an empty body more often
|
|
97
|
+
* than not, which the OpenAI SDK surfaces as an opaque "<status> status code
|
|
98
|
+
* (no body)". Log the status and whatever body text exists (never the
|
|
99
|
+
* request, which carries the bearer token) before handing the response to
|
|
100
|
+
* the SDK. Success responses pass through untouched.
|
|
101
|
+
*/
|
|
102
|
+
function chatGptDiagnosticFetch(logger: Logger): typeof fetch {
|
|
103
|
+
return async (input, init) => {
|
|
104
|
+
const response = await fetch(input, init);
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
let body = '';
|
|
107
|
+
try {
|
|
108
|
+
body = (await response.clone().text()).slice(0, 500);
|
|
109
|
+
} catch {
|
|
110
|
+
// Body unreadable — the status line alone will have to do.
|
|
111
|
+
}
|
|
112
|
+
logger.warn(
|
|
113
|
+
`[byo-chatgpt] backend ${response.status} ${response.statusText}: ${body || '<empty body>'}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return response;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
121
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
122
|
+
? (value as Record<string, unknown>)
|
|
123
|
+
: {};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface CreateByoChatModelArgs {
|
|
127
|
+
credential: ByoCredential;
|
|
128
|
+
/** Provider-native model id (already role-translated by the caller). */
|
|
129
|
+
modelId: string;
|
|
130
|
+
role: ProviderModelRole | string;
|
|
131
|
+
/** Caller overrides, e.g. temperature. `model`/`apiKey` are set here. */
|
|
132
|
+
params?: ChatOpenAIFields;
|
|
133
|
+
logger?: Logger;
|
|
134
|
+
/** ChatGPT lane only: where requests go (default: the real backend). */
|
|
135
|
+
chatGptBackend?: ChatGptBackendConfig;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const NOOP: Logger = {
|
|
139
|
+
log: () => undefined,
|
|
140
|
+
warn: () => undefined,
|
|
141
|
+
error: () => undefined,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build the chat model for a BYO turn. The credential's access token / API
|
|
146
|
+
* key lives only inside the returned client instance — it is never placed on
|
|
147
|
+
* the request context, graph state, or trace metadata.
|
|
148
|
+
*/
|
|
149
|
+
export function createByoChatModel(
|
|
150
|
+
args: CreateByoChatModelArgs,
|
|
151
|
+
): BaseChatModel {
|
|
152
|
+
const { credential, modelId, role, params } = args;
|
|
153
|
+
const logger = args.logger ?? NOOP;
|
|
154
|
+
const backend = args.chatGptBackend ?? DEFAULT_CHATGPT_BACKEND;
|
|
155
|
+
// Guard/classification roles run cold, generative roles match the platform
|
|
156
|
+
// default — same policy as the platform factory. Only the `deepseek` and
|
|
157
|
+
// `gemini` branches send it: GPT-5-family models (the `openai` and
|
|
158
|
+
// `chatgpt` catalogs) and the Claude 5 family reject/deprecate sampling
|
|
159
|
+
// params with a 400, so those branches force `temperature: undefined`
|
|
160
|
+
// AFTER the spreads.
|
|
161
|
+
const temperature = role === 'guard' ? 0 : 0.8;
|
|
162
|
+
// LangChain's AsyncCaller default is SIX retries with exponential backoff
|
|
163
|
+
// — a rate-limited BYO account would leave the user staring at
|
|
164
|
+
// "Thinking..." for a minute before any error surfaces. Two attempts keep
|
|
165
|
+
// transient-blip resilience while failing fast enough to report.
|
|
166
|
+
const maxRetries = 2;
|
|
167
|
+
|
|
168
|
+
switch (credential.provider) {
|
|
169
|
+
case 'openai':
|
|
170
|
+
return new ChatOpenAI({
|
|
171
|
+
__includeRawResponse: true,
|
|
172
|
+
maxRetries,
|
|
173
|
+
...params,
|
|
174
|
+
model: modelId,
|
|
175
|
+
apiKey: credential.apiKey,
|
|
176
|
+
temperature: undefined,
|
|
177
|
+
topP: undefined,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
case 'deepseek':
|
|
181
|
+
return new ChatOpenAI({
|
|
182
|
+
temperature,
|
|
183
|
+
__includeRawResponse: true,
|
|
184
|
+
maxRetries,
|
|
185
|
+
...params,
|
|
186
|
+
model: modelId,
|
|
187
|
+
apiKey: credential.apiKey,
|
|
188
|
+
configuration: {
|
|
189
|
+
baseURL: DEEPSEEK_BASE_URL,
|
|
190
|
+
...asRecord(params?.configuration),
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
case 'gemini':
|
|
195
|
+
return new ChatOpenAI({
|
|
196
|
+
temperature,
|
|
197
|
+
__includeRawResponse: true,
|
|
198
|
+
maxRetries,
|
|
199
|
+
...params,
|
|
200
|
+
model: modelId,
|
|
201
|
+
apiKey: credential.apiKey,
|
|
202
|
+
configuration: {
|
|
203
|
+
baseURL: GEMINI_OPENAI_COMPAT_BASE_URL,
|
|
204
|
+
...asRecord(params?.configuration),
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
case 'anthropic':
|
|
209
|
+
// Claude 5 family: sampling params are rejected — force undefined
|
|
210
|
+
// after the spreads, exactly like the openai/chatgpt branches.
|
|
211
|
+
return new ChatOpenAI({
|
|
212
|
+
__includeRawResponse: true,
|
|
213
|
+
maxRetries,
|
|
214
|
+
...params,
|
|
215
|
+
model: modelId,
|
|
216
|
+
apiKey: credential.apiKey,
|
|
217
|
+
temperature: undefined,
|
|
218
|
+
topP: undefined,
|
|
219
|
+
configuration: {
|
|
220
|
+
baseURL: ANTHROPIC_OPENAI_COMPAT_BASE_URL,
|
|
221
|
+
...asRecord(params?.configuration),
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
case 'chatgpt': {
|
|
226
|
+
// The ChatGPT backend speaks the Responses API only, streamed only,
|
|
227
|
+
// stateless only — `useResponsesApi` + `streaming` + `store: false` are
|
|
228
|
+
// all load-bearing. Auth is the subscription access token (refreshed
|
|
229
|
+
// upstream by WorkersByoService before this is constructed) plus the
|
|
230
|
+
// account-id header from the token's `chatgpt_account_id` claim.
|
|
231
|
+
// `session-id` (Codex spelling) and `session_id` (proxy spelling) are
|
|
232
|
+
// distinct headers upstream — send both. The backend rejects sampling
|
|
233
|
+
// params (`temperature`/`top_p`) the way the standard API does for
|
|
234
|
+
// reasoning models, and reports request errors with an EMPTY body —
|
|
235
|
+
// hence the diagnostic fetch.
|
|
236
|
+
const sessionId = crypto.randomUUID();
|
|
237
|
+
return new ChatOpenAI({
|
|
238
|
+
__includeRawResponse: true,
|
|
239
|
+
maxRetries,
|
|
240
|
+
useResponsesApi: true,
|
|
241
|
+
streaming: true,
|
|
242
|
+
// Zero-data-retention mode matches the backend's mandatory
|
|
243
|
+
// `store: false`: prior-turn reasoning is only replayed when it
|
|
244
|
+
// carries `encrypted_content`, and raw `responseMetadata.output`
|
|
245
|
+
// items (which the stateless backend cannot resolve) are never
|
|
246
|
+
// echoed back. This also keeps foreign-provider reasoning kwargs in
|
|
247
|
+
// checkpointed history out of the request.
|
|
248
|
+
zdrEnabled: true,
|
|
249
|
+
// Effort + human-readable reasoning summaries, exactly the pair the
|
|
250
|
+
// Codex clients send — summaries feed the portal's thinking stream.
|
|
251
|
+
reasoning: { effort: 'medium', summary: 'auto' },
|
|
252
|
+
...params,
|
|
253
|
+
model: modelId,
|
|
254
|
+
apiKey: credential.oauth.accessToken,
|
|
255
|
+
// After the spreads: the backend hard-rejects sampling params.
|
|
256
|
+
temperature: undefined,
|
|
257
|
+
topP: undefined,
|
|
258
|
+
configuration: {
|
|
259
|
+
baseURL: backend.baseUrl,
|
|
260
|
+
fetch: chatGptDiagnosticFetch(logger),
|
|
261
|
+
...asRecord(params?.configuration),
|
|
262
|
+
defaultHeaders: {
|
|
263
|
+
...chatGptBackendHeaders(backend),
|
|
264
|
+
'ChatGPT-Account-ID': credential.oauth.accountId,
|
|
265
|
+
originator: CHATGPT_ORIGINATOR,
|
|
266
|
+
'session-id': sessionId,
|
|
267
|
+
session_id: sessionId,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
modelKwargs: {
|
|
271
|
+
// Stateless mode is mandatory; encrypted reasoning content rides
|
|
272
|
+
// along so multi-turn reasoning survives without server-side state.
|
|
273
|
+
store: false,
|
|
274
|
+
include: ['reasoning.encrypted_content'],
|
|
275
|
+
...asRecord(params?.modelKwargs),
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/byo-llm/*` HTTP surface — the Workers port of the Node runtime's
|
|
3
|
+
* `ByoLlmController`. Served from inside `UserOracleDO` (the object owns the
|
|
4
|
+
* user's credentials, secrets service and refresh state); the shell proxies
|
|
5
|
+
* authenticated requests here, so `userDid` is always the UCAN-authenticated
|
|
6
|
+
* caller.
|
|
7
|
+
*
|
|
8
|
+
* Routes (identical to the Nest controller):
|
|
9
|
+
* GET /byo-llm/status — per-provider connection status
|
|
10
|
+
* POST /byo-llm/chatgpt/device/start — start the device-auth flow
|
|
11
|
+
* POST /byo-llm/chatgpt/device/poll — poll the device grant once
|
|
12
|
+
* GET /byo-llm/chatgpt/authorize-url — pasted-redirect fallback URL
|
|
13
|
+
* POST /byo-llm/chatgpt/exchange — exchange a pasted auth code
|
|
14
|
+
* POST /byo-llm/validate/:provider — live-check a credential
|
|
15
|
+
* PUT /byo-llm/credentials/:provider — save a provider API key
|
|
16
|
+
* DELETE /byo-llm/credentials/:provider — disconnect a provider
|
|
17
|
+
*
|
|
18
|
+
* Error bodies use the Nest wire shape (`{ statusCode, message }`). When BYO
|
|
19
|
+
* is disabled, `status` reports `{ enabled: false }` (as on Node) and every
|
|
20
|
+
* other route 404s.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { Logger } from '../plugin-api/types';
|
|
24
|
+
import { BYO_DEFAULT_MODEL, isByoProvider, toByoModelId } from './byo-catalog';
|
|
25
|
+
import type { WorkersByoService } from './byo-service';
|
|
26
|
+
import {
|
|
27
|
+
buildAuthorizeUrl,
|
|
28
|
+
ChatGptOAuthError,
|
|
29
|
+
createOAuthState,
|
|
30
|
+
createPkcePair,
|
|
31
|
+
exchangeAuthorizationCode,
|
|
32
|
+
pollDeviceToken,
|
|
33
|
+
startDeviceAuthorization,
|
|
34
|
+
} from './chatgpt-oauth';
|
|
35
|
+
|
|
36
|
+
const NOT_ENABLED_MESSAGE =
|
|
37
|
+
'Bring-your-own-credential LLMs are not enabled on this oracle';
|
|
38
|
+
|
|
39
|
+
const NOOP: Logger = {
|
|
40
|
+
log: () => undefined,
|
|
41
|
+
warn: () => undefined,
|
|
42
|
+
error: () => undefined,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function json(payload: unknown, status = 200): Response {
|
|
46
|
+
return Response.json(payload, { status });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function error(status: number, message: string): Response {
|
|
50
|
+
return json({ statusCode: status, message }, status);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function readJsonBody(
|
|
54
|
+
request: Request,
|
|
55
|
+
): Promise<Record<string, unknown>> {
|
|
56
|
+
try {
|
|
57
|
+
const parsed: unknown = await request.json();
|
|
58
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
59
|
+
? (parsed as Record<string, unknown>)
|
|
60
|
+
: {};
|
|
61
|
+
} catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Dispatch one `/byo-llm/*` request. Never throws — unexpected failures
|
|
68
|
+
* become 500s with the Nest error body shape.
|
|
69
|
+
*/
|
|
70
|
+
export async function handleByoRequest(
|
|
71
|
+
byo: WorkersByoService,
|
|
72
|
+
userDid: string,
|
|
73
|
+
request: Request,
|
|
74
|
+
logger: Logger = NOOP,
|
|
75
|
+
): Promise<Response> {
|
|
76
|
+
const url = new URL(request.url);
|
|
77
|
+
const path = url.pathname.replace(/\/+$/, '');
|
|
78
|
+
const method = request.method.toUpperCase();
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
if (path === '/byo-llm/status' && method === 'GET') {
|
|
82
|
+
// Reports { enabled: false } rather than 404 when BYO is off (Node
|
|
83
|
+
// behaviour): the connect UI probes this unconditionally.
|
|
84
|
+
const refresh = url.searchParams.get('refresh');
|
|
85
|
+
return json(
|
|
86
|
+
await byo.status(userDid, {
|
|
87
|
+
refresh: refresh === 'true' || refresh === '1',
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!byo.isEnabled()) return error(404, NOT_ENABLED_MESSAGE);
|
|
93
|
+
|
|
94
|
+
if (path === '/byo-llm/chatgpt/device/start' && method === 'POST') {
|
|
95
|
+
try {
|
|
96
|
+
const authorization = await startDeviceAuthorization(
|
|
97
|
+
byo.chatGptClientId,
|
|
98
|
+
);
|
|
99
|
+
await byo.bindDeviceAuth(userDid, authorization.deviceAuthId);
|
|
100
|
+
return json(authorization);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (
|
|
103
|
+
err instanceof ChatGptOAuthError &&
|
|
104
|
+
err.code === 'device_flow_unavailable'
|
|
105
|
+
) {
|
|
106
|
+
return error(502, err.message);
|
|
107
|
+
}
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (path === '/byo-llm/chatgpt/device/poll' && method === 'POST') {
|
|
113
|
+
const body = await readJsonBody(request);
|
|
114
|
+
const deviceAuthId = body.deviceAuthId;
|
|
115
|
+
const userCode = body.userCode;
|
|
116
|
+
if (
|
|
117
|
+
typeof deviceAuthId !== 'string' ||
|
|
118
|
+
deviceAuthId.length === 0 ||
|
|
119
|
+
typeof userCode !== 'string' ||
|
|
120
|
+
userCode.length === 0
|
|
121
|
+
) {
|
|
122
|
+
return error(400, 'deviceAuthId and userCode are required');
|
|
123
|
+
}
|
|
124
|
+
// The flow must have been started by this account on this oracle — a
|
|
125
|
+
// poll for someone else's (or an unknown) device-auth id can neither
|
|
126
|
+
// complete the grant nor capture the resulting tokens.
|
|
127
|
+
if (!(await byo.isDeviceAuthOwner(userDid, deviceAuthId))) {
|
|
128
|
+
return json({
|
|
129
|
+
status: 'failed',
|
|
130
|
+
error: 'Unknown or expired sign-in attempt — please start again.',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const result = await pollDeviceToken({
|
|
134
|
+
clientId: byo.chatGptClientId,
|
|
135
|
+
deviceAuthId,
|
|
136
|
+
userCode,
|
|
137
|
+
});
|
|
138
|
+
if (result.status === 'pending') return json({ status: 'pending' });
|
|
139
|
+
if (result.status === 'failed') {
|
|
140
|
+
return json({ status: 'failed', error: result.error });
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
await byo.storeChatGptTokens(userDid, result.tokens);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
// The sign-in itself succeeded and the authorization code is spent —
|
|
146
|
+
// failing here would force a full restart of the flow. Hold the
|
|
147
|
+
// tokens instead; the service retries persistence in the background.
|
|
148
|
+
logger.error(
|
|
149
|
+
`[byo] could not persist ChatGPT tokens for ${userDid} — holding: ${err instanceof Error ? err.message : String(err)}`,
|
|
150
|
+
);
|
|
151
|
+
await byo.holdUnpersistedChatGptTokens(userDid, result.tokens);
|
|
152
|
+
}
|
|
153
|
+
logger.log(`[byo] ChatGPT subscription connected for ${userDid}`);
|
|
154
|
+
return json({
|
|
155
|
+
status: 'connected',
|
|
156
|
+
defaultModelId: toByoModelId('chatgpt', BYO_DEFAULT_MODEL.chatgpt),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (path === '/byo-llm/chatgpt/authorize-url' && method === 'GET') {
|
|
161
|
+
const { codeVerifier, codeChallenge } = await createPkcePair();
|
|
162
|
+
const state = createOAuthState();
|
|
163
|
+
return json({
|
|
164
|
+
url: buildAuthorizeUrl({
|
|
165
|
+
clientId: byo.chatGptClientId,
|
|
166
|
+
codeChallenge,
|
|
167
|
+
state,
|
|
168
|
+
}),
|
|
169
|
+
codeVerifier,
|
|
170
|
+
state,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (path === '/byo-llm/chatgpt/exchange' && method === 'POST') {
|
|
175
|
+
const body = await readJsonBody(request);
|
|
176
|
+
const code = body.code;
|
|
177
|
+
const codeVerifier = body.codeVerifier;
|
|
178
|
+
if (
|
|
179
|
+
typeof code !== 'string' ||
|
|
180
|
+
code.length === 0 ||
|
|
181
|
+
typeof codeVerifier !== 'string' ||
|
|
182
|
+
codeVerifier.length === 0
|
|
183
|
+
) {
|
|
184
|
+
return error(400, 'code and codeVerifier are required');
|
|
185
|
+
}
|
|
186
|
+
let tokens;
|
|
187
|
+
try {
|
|
188
|
+
tokens = await exchangeAuthorizationCode({
|
|
189
|
+
clientId: byo.chatGptClientId,
|
|
190
|
+
code,
|
|
191
|
+
codeVerifier,
|
|
192
|
+
});
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (err instanceof ChatGptOAuthError) return error(502, err.message);
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
await byo.storeChatGptTokens(userDid, tokens);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
// Same as the device path: the code is spent and the sign-in worked —
|
|
201
|
+
// hold the tokens rather than failing a completed authentication.
|
|
202
|
+
logger.error(
|
|
203
|
+
`[byo] could not persist ChatGPT tokens for ${userDid} — holding: ${err instanceof Error ? err.message : String(err)}`,
|
|
204
|
+
);
|
|
205
|
+
await byo.holdUnpersistedChatGptTokens(userDid, tokens);
|
|
206
|
+
}
|
|
207
|
+
logger.log(`[byo] ChatGPT subscription connected for ${userDid}`);
|
|
208
|
+
return json({
|
|
209
|
+
connected: true,
|
|
210
|
+
defaultModelId: toByoModelId('chatgpt', BYO_DEFAULT_MODEL.chatgpt),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const validateMatch = /^\/byo-llm\/validate\/([^/]+)$/.exec(path);
|
|
215
|
+
if (validateMatch && method === 'POST') {
|
|
216
|
+
const provider = decodeURIComponent(validateMatch[1]!);
|
|
217
|
+
if (!isByoProvider(provider)) {
|
|
218
|
+
return error(400, `Unknown provider "${provider}"`);
|
|
219
|
+
}
|
|
220
|
+
return json(await byo.validate(userDid, provider));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const credentialsMatch = /^\/byo-llm\/credentials\/([^/]+)$/.exec(path);
|
|
224
|
+
if (credentialsMatch && (method === 'PUT' || method === 'DELETE')) {
|
|
225
|
+
const provider = decodeURIComponent(credentialsMatch[1]!);
|
|
226
|
+
if (!isByoProvider(provider)) {
|
|
227
|
+
return error(400, `Unknown provider "${provider}"`);
|
|
228
|
+
}
|
|
229
|
+
if (method === 'DELETE') {
|
|
230
|
+
await byo.deleteCredential(userDid, provider);
|
|
231
|
+
logger.log(`[byo] credential ${provider} disconnected for ${userDid}`);
|
|
232
|
+
return json({ ok: true, provider });
|
|
233
|
+
}
|
|
234
|
+
if (provider === 'chatgpt') {
|
|
235
|
+
return error(400, 'ChatGPT connects via OAuth, not an API key');
|
|
236
|
+
}
|
|
237
|
+
const body = await readJsonBody(request);
|
|
238
|
+
const apiKey = typeof body.apiKey === 'string' ? body.apiKey.trim() : '';
|
|
239
|
+
if (!apiKey) return error(400, 'API key must not be empty');
|
|
240
|
+
if (apiKey.length > 512) {
|
|
241
|
+
return error(400, 'API key must be at most 512 characters');
|
|
242
|
+
}
|
|
243
|
+
await byo.storeApiKey(userDid, provider, apiKey);
|
|
244
|
+
logger.log(`[byo] API key for ${provider} stored for ${userDid}`);
|
|
245
|
+
return json({ ok: true, provider });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return error(404, `Cannot ${method} ${path}`);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
logger.error(
|
|
251
|
+
`[byo] ${method} ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
252
|
+
);
|
|
253
|
+
return error(500, err instanceof Error ? err.message : 'Internal error');
|
|
254
|
+
}
|
|
255
|
+
}
|