@visual-z/xeno 0.8.4 → 0.9.1
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/CHANGELOG.md +19 -0
- package/README.md +31 -746
- package/bin/xeno.exe +26 -0
- package/install.cjs +88 -0
- package/package.json +18 -48
- package/docs/app-server.md +0 -115
- package/docs/foundations.md +0 -88
- package/docs/install.md +0 -244
- package/docs/providers.md +0 -172
- package/docs/research/foundation-clients-2026-10-09.md +0 -27
- package/docs/research/foundation-editing-2026-10-09.md +0 -56
- package/docs/research/foundation-network-2026-10-09.md +0 -37
- package/docs/research/foundation-workflow-2026-10-09.md +0 -37
- package/src/apps/acp-server.ts +0 -41
- package/src/apps/agent-profiles.ts +0 -19
- package/src/apps/app-server.ts +0 -30
- package/src/apps/approve.ts +0 -49
- package/src/apps/cli.ts +0 -99
- package/src/apps/commands.ts +0 -319
- package/src/apps/completions.ts +0 -87
- package/src/apps/control-transport.ts +0 -90
- package/src/apps/export-cli.ts +0 -24
- package/src/apps/goal-flow.ts +0 -70
- package/src/apps/image-draft.ts +0 -46
- package/src/apps/input.ts +0 -312
- package/src/apps/model-catalog.ts +0 -69
- package/src/apps/preferences.ts +0 -20
- package/src/apps/project-trust.ts +0 -9
- package/src/apps/provider-connect.ts +0 -346
- package/src/apps/question-ui.ts +0 -27
- package/src/apps/render.ts +0 -181
- package/src/apps/resources.ts +0 -66
- package/src/apps/review.ts +0 -109
- package/src/apps/session-hub.ts +0 -96
- package/src/apps/session-lifecycle.ts +0 -64
- package/src/apps/shell.ts +0 -2071
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1818
- package/src/apps/ui/control-model.ts +0 -43
- package/src/apps/ui/extension-host.ts +0 -194
- package/src/apps/ui/image-cache.ts +0 -121
- package/src/apps/ui/palette.ts +0 -88
- package/src/apps/ui/project.ts +0 -127
- package/src/apps/ui/standalone.ts +0 -148
- package/src/apps/ui/status-model.ts +0 -68
- package/src/apps/ui/surface.ts +0 -91
- package/src/apps/ui/theme.ts +0 -86
- package/src/apps/update-check.ts +0 -112
- package/src/apps/workflow.ts +0 -58
- package/src/core/acceptance.ts +0 -139
- package/src/core/action-defaults.ts +0 -58
- package/src/core/activity-status.ts +0 -52
- package/src/core/agent-profile.ts +0 -130
- package/src/core/attachment-input.ts +0 -17
- package/src/core/blind-checks.ts +0 -72
- package/src/core/boundary.ts +0 -174
- package/src/core/branch-receipt.ts +0 -26
- package/src/core/catalog-merge.ts +0 -19
- package/src/core/checkpoints.ts +0 -85
- package/src/core/churn.ts +0 -61
- package/src/core/command-artifacts.ts +0 -94
- package/src/core/command-contract.ts +0 -151
- package/src/core/completion-action.ts +0 -20
- package/src/core/completion-review.ts +0 -98
- package/src/core/context-detail.ts +0 -37
- package/src/core/context-policy.ts +0 -133
- package/src/core/context-window.ts +0 -54
- package/src/core/continuation.ts +0 -130
- package/src/core/control-protocol.ts +0 -69
- package/src/core/control-view.ts +0 -39
- package/src/core/cycle.ts +0 -17
- package/src/core/data-query.ts +0 -201
- package/src/core/deadline.ts +0 -40
- package/src/core/derive.ts +0 -516
- package/src/core/draft-images.ts +0 -56
- package/src/core/edit-plan.ts +0 -200
- package/src/core/environment.ts +0 -181
- package/src/core/event-validation.ts +0 -220
- package/src/core/events.ts +0 -670
- package/src/core/evidence-ladder.ts +0 -62
- package/src/core/evidence-links.ts +0 -68
- package/src/core/evidence.ts +0 -240
- package/src/core/execution.ts +0 -18
- package/src/core/exhaustive.ts +0 -18
- package/src/core/export.ts +0 -179
- package/src/core/extension-flags.ts +0 -14
- package/src/core/external-effects.ts +0 -488
- package/src/core/failure-recovery.ts +0 -173
- package/src/core/file-precondition.ts +0 -78
- package/src/core/file-references.ts +0 -35
- package/src/core/frontmatter.ts +0 -213
- package/src/core/git-status.ts +0 -29
- package/src/core/goal-view.ts +0 -64
- package/src/core/goal.ts +0 -200
- package/src/core/handoff.ts +0 -479
- package/src/core/history-query.ts +0 -139
- package/src/core/history.ts +0 -237
- package/src/core/image-limits.ts +0 -72
- package/src/core/images.ts +0 -46
- package/src/core/integration-health.ts +0 -34
- package/src/core/json-event.ts +0 -131
- package/src/core/json-pointer.ts +0 -46
- package/src/core/keymap.ts +0 -153
- package/src/core/lsp-framing.ts +0 -34
- package/src/core/mcp-config.ts +0 -68
- package/src/core/model-picker.ts +0 -64
- package/src/core/model-preferences.ts +0 -28
- package/src/core/model-replay.ts +0 -22
- package/src/core/model-scope.ts +0 -53
- package/src/core/note-guidance.ts +0 -34
- package/src/core/oauth-fields.ts +0 -44
- package/src/core/observation-view.ts +0 -152
- package/src/core/outcome.ts +0 -44
- package/src/core/overflow.ts +0 -59
- package/src/core/packages.ts +0 -47
- package/src/core/patch.ts +0 -130
- package/src/core/path-suggestions.ts +0 -59
- package/src/core/permissions.ts +0 -410
- package/src/core/process-receipt.ts +0 -105
- package/src/core/process-state.ts +0 -37
- package/src/core/program-status.ts +0 -40
- package/src/core/progress.ts +0 -150
- package/src/core/project.ts +0 -431
- package/src/core/prompt-policy.ts +0 -46
- package/src/core/prompts.ts +0 -111
- package/src/core/question.ts +0 -94
- package/src/core/queue.ts +0 -78
- package/src/core/read-bounds.ts +0 -24
- package/src/core/read-observation.ts +0 -106
- package/src/core/recovery-sources.ts +0 -74
- package/src/core/regex-dialect.ts +0 -25
- package/src/core/request-budget.ts +0 -23
- package/src/core/request-progress.ts +0 -87
- package/src/core/request-state.ts +0 -273
- package/src/core/resource-selection.ts +0 -36
- package/src/core/response-recovery.ts +0 -59
- package/src/core/resume.ts +0 -59
- package/src/core/review.ts +0 -68
- package/src/core/ripgrep-release.ts +0 -64
- package/src/core/runtime-target.ts +0 -94
- package/src/core/scheduling.ts +0 -8
- package/src/core/search-observation.ts +0 -97
- package/src/core/secrets.ts +0 -188
- package/src/core/session-archive.ts +0 -4
- package/src/core/session-browser.ts +0 -82
- package/src/core/session-entry.ts +0 -61
- package/src/core/session-exit.ts +0 -42
- package/src/core/session-format.ts +0 -169
- package/src/core/session-position.ts +0 -26
- package/src/core/session-tree.ts +0 -147
- package/src/core/settings.ts +0 -192
- package/src/core/skills.ts +0 -165
- package/src/core/sql-query.ts +0 -211
- package/src/core/stall.ts +0 -128
- package/src/core/stop-report.ts +0 -68
- package/src/core/text-normalize.ts +0 -81
- package/src/core/theme.ts +0 -166
- package/src/core/thinking.ts +0 -26
- package/src/core/tokens.ts +0 -151
- package/src/core/tool-contract.ts +0 -153
- package/src/core/tool-discovery.ts +0 -87
- package/src/core/tool-failure.ts +0 -81
- package/src/core/tool-input.ts +0 -44
- package/src/core/transcript.ts +0 -108
- package/src/core/tui-mode.ts +0 -29
- package/src/core/update-check.ts +0 -157
- package/src/core/version.ts +0 -55
- package/src/core/web-content.ts +0 -80
- package/src/core/work-state.ts +0 -154
- package/src/core/workflow-mode.ts +0 -34
- package/src/core/working-set.ts +0 -119
- package/src/core/yaml.ts +0 -282
- package/src/runtime/acp-agent.ts +0 -289
- package/src/runtime/agent-profiles.ts +0 -21
- package/src/runtime/auth.ts +0 -367
- package/src/runtime/blind-check.ts +0 -36
- package/src/runtime/branch-jobs.ts +0 -36
- package/src/runtime/catalog.ts +0 -285
- package/src/runtime/clipboard.ts +0 -125
- package/src/runtime/command-artifacts.ts +0 -64
- package/src/runtime/connection-reuse.ts +0 -40
- package/src/runtime/control-client.ts +0 -149
- package/src/runtime/control-connection.ts +0 -139
- package/src/runtime/counter-check.ts +0 -74
- package/src/runtime/environment-tools.ts +0 -60
- package/src/runtime/evidence-notes.ts +0 -35
- package/src/runtime/exec-sessions.ts +0 -456
- package/src/runtime/execution-control.ts +0 -40
- package/src/runtime/extension-loader.ts +0 -37
- package/src/runtime/extension-ui.ts +0 -47
- package/src/runtime/extensions.ts +0 -370
- package/src/runtime/formatter-tool.ts +0 -38
- package/src/runtime/formatter.ts +0 -50
- package/src/runtime/foundation.ts +0 -116
- package/src/runtime/history-cache.ts +0 -57
- package/src/runtime/history-tools.ts +0 -168
- package/src/runtime/images.ts +0 -80
- package/src/runtime/integrations.ts +0 -216
- package/src/runtime/loop.ts +0 -3108
- package/src/runtime/lsp-client.ts +0 -201
- package/src/runtime/mcp-client.ts +0 -264
- package/src/runtime/mcp-oauth.ts +0 -168
- package/src/runtime/mcp-schema.ts +0 -51
- package/src/runtime/model-config.ts +0 -232
- package/src/runtime/model-data.LICENSE +0 -27
- package/src/runtime/model-data.ts +0 -21
- package/src/runtime/model-discovery.ts +0 -208
- package/src/runtime/model-replay.ts +0 -10
- package/src/runtime/model-scope.ts +0 -20
- package/src/runtime/model-sources.ts +0 -151
- package/src/runtime/model.ts +0 -686
- package/src/runtime/models-fetch.ts +0 -57
- package/src/runtime/oauth-device.ts +0 -86
- package/src/runtime/oauth.ts +0 -853
- package/src/runtime/open-image.ts +0 -42
- package/src/runtime/openai-responses.ts +0 -304
- package/src/runtime/openai.ts +0 -697
- package/src/runtime/output-schema.ts +0 -26
- package/src/runtime/package-manager.ts +0 -125
- package/src/runtime/patch-tool.ts +0 -38
- package/src/runtime/permission-resources.ts +0 -18
- package/src/runtime/pi-provider.ts +0 -423
- package/src/runtime/program-status.ts +0 -38
- package/src/runtime/progress.ts +0 -26
- package/src/runtime/provider-access.ts +0 -21
- package/src/runtime/provider-connect.ts +0 -109
- package/src/runtime/provider-presets.ts +0 -98
- package/src/runtime/provider-protocols.ts +0 -29
- package/src/runtime/provider-request.ts +0 -87
- package/src/runtime/provider-tool-policy.ts +0 -33
- package/src/runtime/provider.ts +0 -580
- package/src/runtime/question-tool.ts +0 -62
- package/src/runtime/retry.ts +0 -447
- package/src/runtime/review-tool.ts +0 -48
- package/src/runtime/rich-text.ts +0 -36
- package/src/runtime/runtime-target.ts +0 -149
- package/src/runtime/secrets.ts +0 -79
- package/src/runtime/self-update.ts +0 -74
- package/src/runtime/session-control.ts +0 -265
- package/src/runtime/session-hub.ts +0 -85
- package/src/runtime/stream-idle.ts +0 -80
- package/src/runtime/tool-broker.ts +0 -137
- package/src/runtime/tool-discovery.ts +0 -58
- package/src/runtime/tool-queue.ts +0 -77
- package/src/runtime/tools.ts +0 -1820
- package/src/runtime/turn-effects.ts +0 -31
- package/src/runtime/update-check.ts +0 -48
- package/src/runtime/usage.ts +0 -253
- package/src/runtime/version.ts +0 -12
- package/src/runtime/web-http.ts +0 -56
- package/src/runtime/web-tools.ts +0 -88
- package/src/runtime/working-notes.ts +0 -60
- package/src/runtime/workspace-lifecycle.ts +0 -141
- package/src/store/acceptance.ts +0 -15
- package/src/store/agent-profile-files.ts +0 -13
- package/src/store/atomic.ts +0 -31
- package/src/store/auth-store.ts +0 -50
- package/src/store/blind-checks.ts +0 -32
- package/src/store/blobs.ts +0 -143
- package/src/store/checkpoint-history.ts +0 -32
- package/src/store/command-artifacts.ts +0 -96
- package/src/store/command.ts +0 -111
- package/src/store/config.ts +0 -69
- package/src/store/context-files.ts +0 -436
- package/src/store/file-checkpoints.ts +0 -108
- package/src/store/file-index.ts +0 -174
- package/src/store/foundation-config.ts +0 -24
- package/src/store/git-checkpoints.ts +0 -67
- package/src/store/git-status.ts +0 -15
- package/src/store/history-search.ts +0 -19
- package/src/store/log.ts +0 -725
- package/src/store/lsp-document.ts +0 -30
- package/src/store/mcp-credentials.ts +0 -85
- package/src/store/models.ts +0 -60
- package/src/store/observations.ts +0 -224
- package/src/store/packages.ts +0 -98
- package/src/store/patch.ts +0 -92
- package/src/store/permission-paths.ts +0 -34
- package/src/store/process-receipts.ts +0 -34
- package/src/store/query-source.ts +0 -49
- package/src/store/recovery.ts +0 -44
- package/src/store/replay.ts +0 -76
- package/src/store/resource-index.ts +0 -170
- package/src/store/resources.ts +0 -285
- package/src/store/restore.ts +0 -110
- package/src/store/review.ts +0 -109
- package/src/store/runtime-target.ts +0 -64
- package/src/store/search.ts +0 -728
- package/src/store/secrets.ts +0 -44
- package/src/store/serial.ts +0 -13
- package/src/store/session-archive.ts +0 -165
- package/src/store/session-browser.ts +0 -68
- package/src/store/session-position.ts +0 -27
- package/src/store/sessions.ts +0 -525
- package/src/store/source-cancellation.ts +0 -6
- package/src/store/tool-binaries.ts +0 -90
- package/src/store/trust.ts +0 -82
- package/src/store/update-cache.ts +0 -36
- package/src/store/user-settings.ts +0 -30
- package/src/store/workspace-clone.ts +0 -42
- package/src/store/workspace-observation.ts +0 -113
- package/src/store/workspace.ts +0 -115
- package/src/store/worktree.ts +0 -278
- package/src/tui/ansi-spans.ts +0 -126
- package/src/tui/diff.ts +0 -160
- package/src/tui/exhaustive.ts +0 -14
- package/src/tui/fuzzy.ts +0 -75
- package/src/tui/style.ts +0 -208
- package/src/tui/ui/atomic-draft.ts +0 -121
- package/src/tui/ui/branch-panel.ts +0 -47
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -199
- package/src/tui/ui/control-panel.ts +0 -25
- package/src/tui/ui/control-sidebar.ts +0 -49
- package/src/tui/ui/dialog.ts +0 -398
- package/src/tui/ui/draft.ts +0 -23
- package/src/tui/ui/duration.ts +0 -16
- package/src/tui/ui/edge.ts +0 -186
- package/src/tui/ui/image-size.ts +0 -44
- package/src/tui/ui/ink.ts +0 -210
- package/src/tui/ui/items.ts +0 -42
- package/src/tui/ui/keys.ts +0 -40
- package/src/tui/ui/layout.ts +0 -74
- package/src/tui/ui/list-model.ts +0 -86
- package/src/tui/ui/list-view.ts +0 -172
- package/src/tui/ui/mascot.ts +0 -67
- package/src/tui/ui/messages.ts +0 -518
- package/src/tui/ui/motion.ts +0 -28
- package/src/tui/ui/paths.ts +0 -10
- package/src/tui/ui/pictures.ts +0 -108
- package/src/tui/ui/prompt-history.ts +0 -60
- package/src/tui/ui/screen.ts +0 -579
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
package/src/runtime/mcp-oauth.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/** OAuth is a user-owned repair operation; the SDK owns discovery, PKCE and issuer/resource binding. */
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { auth, checkResourceAllowed, resourceUrlFromServerUrl, StreamableHTTPClientTransport,
|
|
4
|
-
type OAuthClientProvider, type OAuthClientInformationContext, type StoredOAuthClientInformation, type StoredOAuthTokens,
|
|
5
|
-
type OAuthClientMetadata, type OAuthDiscoveryState, type FetchLike } from "@modelcontextprotocol/client";
|
|
6
|
-
import type { McpOAuthConfig } from "../core/mcp-config.ts";
|
|
7
|
-
import { isJsonValue } from "../core/tool-contract.ts";
|
|
8
|
-
import type { McpCredentials, McpCredentialsStore } from "../store/mcp-credentials.ts";
|
|
9
|
-
import { boundedResponseBytes } from "./web-http.ts";
|
|
10
|
-
|
|
11
|
-
export class McpAuthRequired extends Error { constructor() { super("MCP authentication requires an explicit login"); this.name = "McpAuthRequired"; } }
|
|
12
|
-
/** The SDK warns with OAuth error_description; normalize peer errors before they can reach its console path. */
|
|
13
|
-
export function safeOAuthFetch(fetcher: FetchLike): FetchLike {
|
|
14
|
-
return async (input, init) => {
|
|
15
|
-
let response: Response;
|
|
16
|
-
try { response = await fetcher(input, init); } catch { throw new McpAuthRequired(); }
|
|
17
|
-
if (response.ok) return response;
|
|
18
|
-
let code = "server_error";
|
|
19
|
-
try {
|
|
20
|
-
const value: unknown = JSON.parse(new TextDecoder().decode(await boundedResponseBytes(response, 16 * 1024)));
|
|
21
|
-
if (value && typeof value === "object" && "error" in value && typeof value.error === "string"
|
|
22
|
-
&& ["invalid_request", "invalid_client", "invalid_grant", "unauthorized_client", "unsupported_grant_type", "invalid_scope", "access_denied", "server_error", "temporarily_unavailable"].includes(value.error)) code = value.error;
|
|
23
|
-
} catch { /* Unknown peer bodies contribute status only, never diagnostics containing secrets. */ }
|
|
24
|
-
return Response.json({ error: code, error_description: "OAuth request rejected" }, { status: response.status, headers: { "content-type": "application/json" } });
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
function savedClient(value: unknown): value is StoredOAuthClientInformation {
|
|
28
|
-
return !!value && typeof value === "object" && "client_id" in value && typeof value.client_id === "string";
|
|
29
|
-
}
|
|
30
|
-
function savedTokens(value: unknown): value is StoredOAuthTokens {
|
|
31
|
-
return !!value && typeof value === "object" && "access_token" in value && typeof value.access_token === "string"
|
|
32
|
-
&& "token_type" in value && typeof value.token_type === "string";
|
|
33
|
-
}
|
|
34
|
-
/** A per-resource provider; discovery/verifier/state exist only during this owned process's login. */
|
|
35
|
-
export class McpOAuthProvider implements OAuthClientProvider {
|
|
36
|
-
private readonly url: string;
|
|
37
|
-
private readonly config: McpOAuthConfig;
|
|
38
|
-
private readonly store: McpCredentialsStore;
|
|
39
|
-
private readonly env: Readonly<Record<string, string | undefined>>;
|
|
40
|
-
private credentials: McpCredentials;
|
|
41
|
-
private verifier: string | undefined;
|
|
42
|
-
private discovery: OAuthDiscoveryState | undefined;
|
|
43
|
-
private expectedState: string | undefined;
|
|
44
|
-
private redirect: ((url: URL) => void | Promise<void>) | undefined;
|
|
45
|
-
private refreshing: Promise<void> | undefined;
|
|
46
|
-
private resourceMetadataUrl: URL | undefined;
|
|
47
|
-
private challengedScope: string | undefined;
|
|
48
|
-
private constructor(url: string, config: McpOAuthConfig,
|
|
49
|
-
store: McpCredentialsStore, env: Readonly<Record<string, string | undefined>>, credentials?: McpCredentials) {
|
|
50
|
-
this.url = url; this.config = config; this.store = store; this.env = env;
|
|
51
|
-
this.credentials = credentials ?? { version: 1, serverUrl: url, redirectUrl: config.redirectUrl ?? "http://127.0.0.1:0/callback",
|
|
52
|
-
clients: {}, tokens: {}, expiresAt: {}, activeIssuer: null, ...(config.clientId ? { clientIdentity: config.clientId } : {}) };
|
|
53
|
-
if (config.redirectUrl) this.credentials.redirectUrl = config.redirectUrl;
|
|
54
|
-
}
|
|
55
|
-
static async create(url: string, config: McpOAuthConfig, store: McpCredentialsStore, env: Readonly<Record<string, string | undefined>>) {
|
|
56
|
-
return new McpOAuthProvider(url, config, store, env, await store.read(url, config.clientId));
|
|
57
|
-
}
|
|
58
|
-
get redirectUrl() { return this.credentials.redirectUrl; }
|
|
59
|
-
get clientMetadata(): OAuthClientMetadata {
|
|
60
|
-
return { client_name: "xeno", application_type: "native", redirect_uris: [this.redirectUrl], response_types: ["code"],
|
|
61
|
-
grant_types: ["authorization_code", "refresh_token"], token_endpoint_auth_method: this.config.clientSecretEnv ? "client_secret_basic" : "none",
|
|
62
|
-
...(this.config.scope ? { scope: this.config.scope } : {}) };
|
|
63
|
-
}
|
|
64
|
-
clientInformation(ctx?: OAuthClientInformationContext): StoredOAuthClientInformation | undefined {
|
|
65
|
-
if (this.config.clientId) {
|
|
66
|
-
const secret = this.config.clientSecretEnv ? this.env[this.config.clientSecretEnv] : undefined;
|
|
67
|
-
if (this.config.clientSecretEnv && !secret) throw new McpAuthRequired();
|
|
68
|
-
return { client_id: this.config.clientId, ...(secret ? { client_secret: secret } : {}), ...(ctx ? { issuer: ctx.issuer } : {}) };
|
|
69
|
-
}
|
|
70
|
-
const value = ctx ? this.credentials.clients[ctx.issuer] : undefined;
|
|
71
|
-
return savedClient(value) ? value : undefined;
|
|
72
|
-
}
|
|
73
|
-
async saveClientInformation(info: StoredOAuthClientInformation, ctx?: OAuthClientInformationContext) {
|
|
74
|
-
const issuer = ctx?.issuer ?? info.issuer;
|
|
75
|
-
if (!issuer || !isJsonValue(info)) throw new McpAuthRequired();
|
|
76
|
-
this.credentials.clients[issuer] = info;
|
|
77
|
-
await this.store.write(this.credentials);
|
|
78
|
-
}
|
|
79
|
-
tokens(ctx?: OAuthClientInformationContext): StoredOAuthTokens | undefined {
|
|
80
|
-
const issuer = ctx?.issuer ?? this.credentials.activeIssuer, value = issuer ? this.credentials.tokens[issuer] : undefined;
|
|
81
|
-
return savedTokens(value) ? value : undefined;
|
|
82
|
-
}
|
|
83
|
-
async saveTokens(tokens: StoredOAuthTokens, ctx?: OAuthClientInformationContext) {
|
|
84
|
-
const issuer = ctx?.issuer ?? tokens.issuer;
|
|
85
|
-
if (!issuer || !isJsonValue(tokens)) throw new McpAuthRequired();
|
|
86
|
-
this.credentials.tokens[issuer] = tokens; this.credentials.activeIssuer = issuer;
|
|
87
|
-
if (tokens.expires_in !== undefined) this.credentials.expiresAt[issuer] = Date.now() + tokens.expires_in * 1_000;
|
|
88
|
-
else delete this.credentials.expiresAt[issuer];
|
|
89
|
-
await this.store.write(this.credentials);
|
|
90
|
-
}
|
|
91
|
-
state() { this.expectedState = randomUUID(); return this.expectedState; }
|
|
92
|
-
redirectToAuthorization(url: URL) {
|
|
93
|
-
if (!this.redirect) throw new McpAuthRequired();
|
|
94
|
-
return this.redirect(url);
|
|
95
|
-
}
|
|
96
|
-
saveCodeVerifier(verifier: string) { this.verifier = verifier; }
|
|
97
|
-
codeVerifier() { if (!this.verifier) throw new McpAuthRequired(); return this.verifier; }
|
|
98
|
-
saveDiscoveryState(state: OAuthDiscoveryState) { this.discovery = state; }
|
|
99
|
-
discoveryState() { return this.discovery; }
|
|
100
|
-
setChallenge(resourceMetadataUrl: URL | undefined, scope: string | undefined) {
|
|
101
|
-
this.resourceMetadataUrl = resourceMetadataUrl; this.challengedScope = scope; this.discovery = undefined;
|
|
102
|
-
}
|
|
103
|
-
private authOptions(fetcher: FetchLike) {
|
|
104
|
-
const scope = this.config.scope ?? this.challengedScope;
|
|
105
|
-
return { serverUrl: this.url, fetchFn: safeOAuthFetch(fetcher), ...(this.resourceMetadataUrl ? { resourceMetadataUrl: this.resourceMetadataUrl } : {}), ...(scope ? { scope } : {}) };
|
|
106
|
-
}
|
|
107
|
-
async validateResourceURL(serverUrl: string | URL, resource?: string) {
|
|
108
|
-
const expected = resourceUrlFromServerUrl(serverUrl);
|
|
109
|
-
if (resource && !checkResourceAllowed({ requestedResource: expected, configuredResource: resource })) throw new McpAuthRequired();
|
|
110
|
-
return expected;
|
|
111
|
-
}
|
|
112
|
-
async invalidateCredentials(scope: "all" | "client" | "tokens" | "verifier" | "discovery") {
|
|
113
|
-
if (scope === "all" || scope === "client") this.credentials.clients = {};
|
|
114
|
-
if (scope === "all" || scope === "tokens") { this.credentials.tokens = {}; this.credentials.expiresAt = {}; this.credentials.activeIssuer = null; }
|
|
115
|
-
if (scope === "all" || scope === "verifier") this.verifier = undefined;
|
|
116
|
-
if (scope === "all" || scope === "discovery") this.discovery = undefined;
|
|
117
|
-
if (scope === "all") await this.store.remove(this.url, this.config.clientId); else await this.store.write(this.credentials);
|
|
118
|
-
}
|
|
119
|
-
async token(fetcher: FetchLike): Promise<string | undefined> {
|
|
120
|
-
const issuer = this.credentials.activeIssuer, expiresAt = issuer ? this.credentials.expiresAt[issuer] : undefined;
|
|
121
|
-
// Assumed 30s skew prevents sending a token that expires while the new request is in flight.
|
|
122
|
-
if (expiresAt !== undefined && expiresAt <= Date.now() + 30_000) {
|
|
123
|
-
if (!this.refreshing) {
|
|
124
|
-
this.refreshing = (async () => {
|
|
125
|
-
if (!this.tokens()?.refresh_token) { await this.invalidateCredentials("tokens"); throw new McpAuthRequired(); }
|
|
126
|
-
try { if (await auth(this, this.authOptions(fetcher)) !== "AUTHORIZED") throw new McpAuthRequired(); }
|
|
127
|
-
catch { await this.invalidateCredentials("tokens"); throw new McpAuthRequired(); }
|
|
128
|
-
})().finally(() => { this.refreshing = undefined; });
|
|
129
|
-
}
|
|
130
|
-
await this.refreshing;
|
|
131
|
-
}
|
|
132
|
-
return this.tokens()?.access_token;
|
|
133
|
-
}
|
|
134
|
-
async login(onAuthorizationUrl: (url: URL) => void | Promise<void>, fetcher: FetchLike, signal: AbortSignal): Promise<void> {
|
|
135
|
-
if (this.redirect) throw new McpAuthRequired();
|
|
136
|
-
signal.throwIfAborted();
|
|
137
|
-
const callback = new URL(this.redirectUrl);
|
|
138
|
-
let resolveCallback: (value: URLSearchParams) => void = () => {}, rejectCallback: (error: Error) => void = () => {};
|
|
139
|
-
const received = new Promise<URLSearchParams>((resolve, reject) => { resolveCallback = resolve; rejectCallback = reject; });
|
|
140
|
-
// Reject handlers are installed before starting auth; cancellation never leaves an unhandled callback promise.
|
|
141
|
-
void received.catch(() => {});
|
|
142
|
-
const server = Bun.serve({ hostname: "127.0.0.1", port: Number(callback.port), fetch: request => {
|
|
143
|
-
const current = new URL(request.url);
|
|
144
|
-
if (current.pathname !== callback.pathname) return new Response("Not found", { status: 404 });
|
|
145
|
-
if (request.method !== "GET" || !this.expectedState || current.searchParams.get("state") !== this.expectedState) return new Response("Invalid authorization state", { status: 400 });
|
|
146
|
-
if (current.searchParams.has("error") || !current.searchParams.get("code")) {
|
|
147
|
-
rejectCallback(new McpAuthRequired()); return new Response("Authorization was not completed", { status: 400 });
|
|
148
|
-
}
|
|
149
|
-
resolveCallback(current.searchParams); return new Response("Authorization received. Return to xeno.", { headers: { "content-type": "text/plain" } });
|
|
150
|
-
} });
|
|
151
|
-
const selected = new URL(callback.pathname, server.url).href;
|
|
152
|
-
if (selected !== this.credentials.redirectUrl && !this.config.clientId) this.credentials.clients = {};
|
|
153
|
-
this.credentials.redirectUrl = selected; this.redirect = onAuthorizationUrl;
|
|
154
|
-
const onAbort = () => rejectCallback(new McpAuthRequired());
|
|
155
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
156
|
-
try {
|
|
157
|
-
const safeFetch = safeOAuthFetch(fetcher);
|
|
158
|
-
const result = await auth(this, this.authOptions(fetcher));
|
|
159
|
-
if (result === "AUTHORIZED") return;
|
|
160
|
-
const params = await received;
|
|
161
|
-
const transport = new StreamableHTTPClientTransport(new URL(this.url), { authProvider: this, fetch: safeFetch, onInsufficientScope: "throw" });
|
|
162
|
-
try { await transport.finishAuth(params); } finally { await transport.close(); }
|
|
163
|
-
} finally {
|
|
164
|
-
signal.removeEventListener("abort", onAbort); server.stop(true); this.redirect = undefined;
|
|
165
|
-
this.expectedState = undefined; this.verifier = undefined; this.discovery = undefined;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/** MCP 2025-11-25 defaults to 2020-12; validation never repairs the frozen invocation. */
|
|
2
|
-
import { Ajv, type ValidateFunction } from "ajv";
|
|
3
|
-
import { Ajv2019 } from "ajv/dist/2019.js";
|
|
4
|
-
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
5
|
-
import { addFormats } from "@modelcontextprotocol/client/validators/ajv";
|
|
6
|
-
import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator } from "@modelcontextprotocol/client";
|
|
7
|
-
import { NOT_STARTED, ToolFault } from "../core/tool-failure.ts";
|
|
8
|
-
|
|
9
|
-
export class McpSchemaError extends Error {
|
|
10
|
-
constructor() { super("MCP schema is invalid or uses an unsupported dialect, reference, keyword or format; repair the server schema before reloading."); this.name = "McpSchemaError"; }
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** The official client's format package is bundled in its public validator entry; no extra dependency. */
|
|
14
|
-
export class McpSchemas implements jsonSchemaValidator {
|
|
15
|
-
private readonly compiled = new Map<string, ValidateFunction>();
|
|
16
|
-
validator(schema: unknown): ValidateFunction {
|
|
17
|
-
if (!schema || typeof schema !== "object" || Array.isArray(schema)) throw new McpSchemaError();
|
|
18
|
-
const key = JSON.stringify(schema), cached = this.compiled.get(key);
|
|
19
|
-
if (cached) return cached;
|
|
20
|
-
try {
|
|
21
|
-
const dialect = (schema as Record<string, unknown>)["$schema"];
|
|
22
|
-
const options = { strictSchema: true, strictNumbers: true, strictTypes: false, strictTuples: false, strictRequired: false,
|
|
23
|
-
validateFormats: true, coerceTypes: false, useDefaults: false, removeAdditional: false, allErrors: false, logger: false as const };
|
|
24
|
-
// Strict schema rejects unknown semantics, while strict style checks are disabled:
|
|
25
|
-
// valid JSON Schema need not spell out every tuple bound or repeat inherited types.
|
|
26
|
-
const ajv = dialect === undefined || dialect === "https://json-schema.org/draft/2020-12/schema" ? new Ajv2020(options)
|
|
27
|
-
: dialect === "https://json-schema.org/draft/2019-09/schema" ? new Ajv2019(options)
|
|
28
|
-
: dialect === "http://json-schema.org/draft-07/schema#" || dialect === "http://json-schema.org/draft-07/schema" ? new Ajv(options)
|
|
29
|
-
: undefined;
|
|
30
|
-
if (!ajv) throw new McpSchemaError();
|
|
31
|
-
addFormats(ajv);
|
|
32
|
-
// MCP's header annotation is interpreted by the official transport, not a validation constraint.
|
|
33
|
-
ajv.addKeyword({ keyword: "x-mcp-header", schemaType: "string", valid: true });
|
|
34
|
-
// Ajv validates this entire JSON schema; the assertion only bridges the SDK's wider wire type.
|
|
35
|
-
const validate = ajv.compile(structuredClone(schema) as JsonSchemaType);
|
|
36
|
-
this.compiled.set(key, validate); return validate;
|
|
37
|
-
} catch { throw new McpSchemaError(); }
|
|
38
|
-
}
|
|
39
|
-
getValidator<T>(schema: JsonSchemaType): JsonSchemaValidator<T> {
|
|
40
|
-
const validate = this.validator(schema);
|
|
41
|
-
return input => validate(input) ? { valid: true, data: input as T, errorMessage: undefined }
|
|
42
|
-
: { valid: false, data: undefined, errorMessage: "MCP structured output failed the advertised schema." };
|
|
43
|
-
}
|
|
44
|
-
prepare(schema: unknown): (input: unknown) => unknown {
|
|
45
|
-
const validate = this.validator(schema);
|
|
46
|
-
return input => {
|
|
47
|
-
if (!validate(input)) throw new ToolFault("invalid_input", "MCP arguments do not satisfy the complete advertised schema. Correct the input before calling the remote tool.", NOT_STARTED);
|
|
48
|
-
return input;
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/** Config parsing owns provider wire vocabulary; identity/filter policy lives in core. */
|
|
2
|
-
import { mergeCatalogs, modelEnabled } from "../core/catalog-merge.ts";
|
|
3
|
-
import { modelKey } from "../core/session-position.ts";
|
|
4
|
-
import { THINKING_LEVELS } from "../core/thinking.ts";
|
|
5
|
-
import { SOURCE_PROVIDERS } from "./model-data.ts";
|
|
6
|
-
import { DEFAULT_COMPAT, type ModelSpec, type Effort } from "./model.ts";
|
|
7
|
-
import { isProviderApi, SDK_APIS } from "./provider-protocols.ts";
|
|
8
|
-
import { providerAccess, setProviderAccess, type ProviderAccess } from "./provider-access.ts";
|
|
9
|
-
|
|
10
|
-
function object(value: unknown): value is Record<string, unknown> { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
11
|
-
function own(value: Record<string, unknown>, key: string): boolean { return Object.hasOwn(value, key); }
|
|
12
|
-
function positive(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value > 0; }
|
|
13
|
-
function nonnegative(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
|
|
14
|
-
function error(path: string, message: string): never { throw new Error(`${path}: ${message}`); }
|
|
15
|
-
function strings(value: unknown, path: string): string[] | undefined {
|
|
16
|
-
if (value === undefined) return undefined;
|
|
17
|
-
if (!Array.isArray(value) || !value.every(x => typeof x === "string")) error(path, "must be an array of strings");
|
|
18
|
-
return value as string[];
|
|
19
|
-
}
|
|
20
|
-
function stringMap(value: unknown, path: string, nullable = false): Record<string, string | null> | undefined {
|
|
21
|
-
if (value === undefined) return undefined;
|
|
22
|
-
if (!object(value) || !Object.values(value).every(x => typeof x === "string" || (nullable && x === null))) error(path, "must contain string values" + (nullable ? " or null" : ""));
|
|
23
|
-
return Object.fromEntries(Object.entries(value as Record<string, string | null>).map(([key, v]) => [key, typeof v === "string" ? configValue(v) : v]));
|
|
24
|
-
}
|
|
25
|
-
/** OpenCode's env reference and Pi's interpolation share one resolver. No implicit uppercase lookup. */
|
|
26
|
-
function configValue(value: string): string { return value.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, "$${$1}"); }
|
|
27
|
-
const RESERVED_BODY = ["model", "modelId", "messages", "system", "systemInstruction", "system_instruction", "instructions", "input", "contents", "context", "tools", "toolConfig", "stream", "previous_response_id", "store", "conversation", "conversation_id", "sessionId", "session_id", "httpOptions", "http_options", "headers", "apiKey", "authToken", "baseUrl", "baseURL", "fetch", "abortSignal", "signal", "onPayload", "onResponse"];
|
|
28
|
-
const REQUEST_OPTIONS = ["temperature", "samplingParams", "cacheRetention", "timeoutMs", "maxRetryDelayMs", "metadata"];
|
|
29
|
-
|
|
30
|
-
function payloadPatch(value: unknown, path: string): Record<string, unknown> | undefined {
|
|
31
|
-
if (value === undefined) return undefined;
|
|
32
|
-
if (!object(value)) error(path, "must be an object");
|
|
33
|
-
for (const [key, child] of Object.entries(value)) {
|
|
34
|
-
if (RESERVED_BODY.includes(key)) error(`${path}.${key}`, "is owned by the log projection and request adapter");
|
|
35
|
-
if ((key === "config" || key === "options") && !object(child)) error(`${path}.${key}`, "must be an object");
|
|
36
|
-
if (object(child)) payloadPatch(child, `${path}.${key}`);
|
|
37
|
-
}
|
|
38
|
-
return value;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function accessPatch(raw: Record<string, unknown>, path: string, parent: ProviderAccess = {}): ProviderAccess {
|
|
42
|
-
if (raw["options"] !== undefined && !object(raw["options"])) error(`${path}.options`, "must be an object");
|
|
43
|
-
const options = object(raw["options"]) ? raw["options"] : {};
|
|
44
|
-
const apiKey = own(raw, "apiKey") ? raw["apiKey"] : options["apiKey"];
|
|
45
|
-
if (apiKey !== undefined && (typeof apiKey !== "string" || !apiKey.length)) error(`${path}.apiKey`, "must be a non-empty string; use auth: false for a keyless endpoint");
|
|
46
|
-
const auth = raw["auth"];
|
|
47
|
-
if (auth !== undefined && auth !== false && auth !== "ambient") error(`${path}.auth`, "must be false or ambient");
|
|
48
|
-
const authHeader = raw["authHeader"];
|
|
49
|
-
if (authHeader !== undefined && typeof authHeader !== "boolean" && (typeof authHeader !== "string" || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(authHeader))) error(`${path}.authHeader`, "must be a boolean or header name");
|
|
50
|
-
const headers = stringMap(raw["headers"] ?? options["headers"] ?? raw["defaultHeaders"], `${path}.headers`, true);
|
|
51
|
-
if (headers && Object.keys(headers).some(key => !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(key))) error(`${path}.headers`, "contains an invalid header name");
|
|
52
|
-
const env = stringMap(raw["env"], `${path}.env`) as Record<string, string> | undefined;
|
|
53
|
-
if (env && Object.keys(env).some(key => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))) error(`${path}.env`, "contains an invalid variable name");
|
|
54
|
-
const body = payloadPatch(raw["body"] ?? raw["extraBody"] ?? options["body"] ?? options["extraBody"], `${path}.body`);
|
|
55
|
-
const requestOptions = Object.fromEntries(Object.entries(options).filter(([key]) => !["apiKey", "baseURL", "baseUrl", "headers", "body", "extraBody"].includes(key)));
|
|
56
|
-
for (const key of Object.keys(requestOptions)) {
|
|
57
|
-
if (!REQUEST_OPTIONS.includes(key)) error(`${path}.options.${key}`, "is not a configurable request option; use env for native cloud settings or body for API parameters");
|
|
58
|
-
}
|
|
59
|
-
payloadPatch(requestOptions["samplingParams"], `${path}.options.samplingParams`);
|
|
60
|
-
for (const key of ["temperature", "timeoutMs", "maxRetryDelayMs"]) if (requestOptions[key] !== undefined && !nonnegative(requestOptions[key])) error(`${path}.options.${key}`, "must be a finite nonnegative number");
|
|
61
|
-
if (requestOptions["cacheRetention"] !== undefined && !["none", "short", "long"].includes(String(requestOptions["cacheRetention"]))) error(`${path}.options.cacheRetention`, "must be none, short or long");
|
|
62
|
-
stringMap(requestOptions["metadata"], `${path}.options.metadata`);
|
|
63
|
-
return { ...parent,
|
|
64
|
-
...(typeof apiKey === "string" ? { apiKey: configValue(apiKey) } : {}),
|
|
65
|
-
...(auth === false || auth === "ambient" ? { auth } : {}),
|
|
66
|
-
...(typeof authHeader === "boolean" || typeof authHeader === "string" ? { authHeader } : {}),
|
|
67
|
-
...(headers ? { headers: { ...parent.headers, ...headers } } : {}),
|
|
68
|
-
...(env ? { env: { ...parent.env, ...env } } : {}),
|
|
69
|
-
...(object(body) ? { body: { ...parent.body, ...body } } : {}),
|
|
70
|
-
...(Object.keys(requestOptions).length ? { options: { ...parent.options, ...requestOptions } } : {}) };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function modelPatch(raw: Record<string, unknown>, base: ModelSpec | undefined, provider: string, id: string, path: string, partial: boolean): ModelSpec {
|
|
74
|
-
const options = object(raw["options"]) ? raw["options"] : {};
|
|
75
|
-
const endpoint = raw["baseUrl"] ?? raw["baseURL"] ?? options["baseURL"] ?? options["baseUrl"];
|
|
76
|
-
if (raw["npm"] !== undefined && (typeof raw["npm"] !== "string" || !SDK_APIS[raw["npm"]]) && raw["api"] === undefined) error(`${path}.npm`, "unsupported SDK; select a supported api explicitly");
|
|
77
|
-
const api = raw["api"] ?? (typeof raw["npm"] === "string" ? SDK_APIS[raw["npm"]] : undefined) ?? base?.api ?? SOURCE_PROVIDERS[provider]?.api;
|
|
78
|
-
if (!isProviderApi(api)) error(`${path}.api`, "unknown or missing protocol");
|
|
79
|
-
if (endpoint !== undefined && typeof endpoint !== "string") error(`${path}.baseUrl`, "must be a string");
|
|
80
|
-
const spec: ModelSpec = { ...(base ?? {}), provider, id, name: base?.name ?? id, api,
|
|
81
|
-
reasoning: base?.reasoning ?? false, ...(partial ? {} : { driver: "pi" }),
|
|
82
|
-
...(typeof endpoint === "string" ? { baseUrl: configValue(endpoint) } : base?.baseUrl !== undefined ? {} : SOURCE_PROVIDERS[provider]?.baseUrl ? { baseUrl: SOURCE_PROVIDERS[provider].baseUrl } : {}) };
|
|
83
|
-
if (raw["api"] !== undefined && raw["api"] !== base?.api) { delete spec.compat; delete spec.nativeCompat; delete spec.thinkingLevelMap; }
|
|
84
|
-
if (typeof raw["name"] === "string" && raw["name"].length) spec.name = raw["name"];
|
|
85
|
-
if (own(raw, "reasoning")) { if (typeof raw["reasoning"] !== "boolean") error(`${path}.reasoning`, "must be boolean"); spec.reasoning = raw["reasoning"]; }
|
|
86
|
-
const limit = object(raw["limit"]) ? raw["limit"] : {};
|
|
87
|
-
for (const [key, value] of [["contextWindow", raw["contextWindow"] ?? limit["context"]], ["maxTokens", raw["maxTokens"] ?? limit["output"]]] as const) {
|
|
88
|
-
if (value !== undefined) { if (!positive(value)) error(`${path}.${key}`, "must be a finite positive number"); spec[key] = value; }
|
|
89
|
-
}
|
|
90
|
-
if (own(raw, "input")) {
|
|
91
|
-
if (!Array.isArray(raw["input"]) || !raw["input"].every(x => x === "text" || x === "image")) error(`${path}.input`, "must contain only text or image");
|
|
92
|
-
spec.input = raw["input"] as ("text" | "image")[];
|
|
93
|
-
}
|
|
94
|
-
if (raw["credentialEnv"] !== undefined) {
|
|
95
|
-
const names = strings(raw["credentialEnv"], `${path}.credentialEnv`)!;
|
|
96
|
-
if (names.some(name => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))) error(`${path}.credentialEnv`, "must contain variable names");
|
|
97
|
-
spec.credentialEnv = names;
|
|
98
|
-
}
|
|
99
|
-
const price = raw["price"] ?? raw["cost"];
|
|
100
|
-
if (price !== undefined) {
|
|
101
|
-
if (!object(price) || !nonnegative(price["input"]) || !nonnegative(price["output"]) || ["cacheRead", "cacheWrite"].some(k => own(price, k) && !nonnegative(price[k]))) error(`${path}.price`, "requires finite nonnegative input/output and optional cache prices");
|
|
102
|
-
spec.price = { input: price["input"], output: price["output"],
|
|
103
|
-
...(nonnegative(price["cacheRead"]) ? { cacheRead: price["cacheRead"] } : {}),
|
|
104
|
-
...(nonnegative(price["cacheWrite"]) ? { cacheWrite: price["cacheWrite"] } : {}) };
|
|
105
|
-
}
|
|
106
|
-
if (raw["cache"] !== undefined) { if (raw["cache"] !== "explicit" && raw["cache"] !== "automatic") error(`${path}.cache`, "must be explicit or automatic"); spec.cache = raw["cache"]; }
|
|
107
|
-
if (raw["rates"] !== undefined) {
|
|
108
|
-
const rates = raw["rates"];
|
|
109
|
-
if (!object(rates) || !nonnegative(rates["read"]) || !nonnegative(rates["write"])) error(`${path}.rates`, "requires finite nonnegative read/write");
|
|
110
|
-
spec.rates = { read: rates["read"], write: rates["write"] };
|
|
111
|
-
}
|
|
112
|
-
if (raw["thinkingLevelMap"] !== undefined) {
|
|
113
|
-
const map = raw["thinkingLevelMap"];
|
|
114
|
-
if (!object(map) || !Object.entries(map).every(([key, value]) => (THINKING_LEVELS as readonly string[]).includes(key) && (value === null || typeof value === "string"))) error(`${path}.thinkingLevelMap`, "requires supported effort keys and string/null values");
|
|
115
|
-
spec.thinkingLevelMap = map as Partial<Record<Effort, string | null>>;
|
|
116
|
-
}
|
|
117
|
-
for (const key of ["compat", "nativeCompat", "samplingParams"] as const) {
|
|
118
|
-
const value = raw[key];
|
|
119
|
-
if (value !== undefined) {
|
|
120
|
-
if (!object(value)) error(`${path}.${key}`, "must be an object");
|
|
121
|
-
if (key === "samplingParams") payloadPatch(value, `${path}.${key}`);
|
|
122
|
-
else for (const [field, flag] of Object.entries(value)) {
|
|
123
|
-
if (/^(supports|requires)/.test(field) && typeof flag !== "boolean") error(`${path}.${key}.${field}`, "must be boolean");
|
|
124
|
-
if (field === "maxTokensField" && flag !== "max_tokens" && flag !== "max_completion_tokens") error(`${path}.${key}.${field}`, "must be max_tokens or max_completion_tokens");
|
|
125
|
-
}
|
|
126
|
-
if (key === "compat") {
|
|
127
|
-
spec.nativeCompat = { ...spec.nativeCompat, ...value };
|
|
128
|
-
spec.compat = { ...DEFAULT_COMPAT, ...base?.compat, ...value } as NonNullable<ModelSpec["compat"]>;
|
|
129
|
-
} else spec[key] = { ...spec[key], ...value };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (raw["driver"] === "pi") spec.driver = "pi";
|
|
133
|
-
if (typeof raw["wireId"] === "string") spec.wireId = raw["wireId"];
|
|
134
|
-
return spec;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function parseModelConfiguration(json: unknown, builtin: readonly ModelSpec[] = []): { models: ModelSpec[]; errors: string[] } {
|
|
138
|
-
let models = [...builtin];
|
|
139
|
-
const errors: string[] = [];
|
|
140
|
-
const report = (err: unknown) => errors.push(err instanceof Error ? err.message : "invalid model configuration");
|
|
141
|
-
if (!Array.isArray(json) && !object(json)) return { models, errors: ["user catalog must be an array or an object with models/providers/provider"] };
|
|
142
|
-
const root = object(json) ? json : {};
|
|
143
|
-
const rows = Array.isArray(json) ? json : root["models"];
|
|
144
|
-
if (rows !== undefined && !Array.isArray(rows)) errors.push("user catalog .models is not an array");
|
|
145
|
-
const indexed = new Map(models.map(model => [modelKey(model), model]));
|
|
146
|
-
if (Array.isArray(rows)) for (const [i, row] of rows.entries()) {
|
|
147
|
-
const path = `models[${i}]` + (object(row) && typeof row["id"] === "string" ? ` (${row["id"]})` : "");
|
|
148
|
-
try {
|
|
149
|
-
if (!object(row)) error(path, "must be an object");
|
|
150
|
-
if (typeof row["id"] !== "string" || !row["id"].length) error(path, "has no id");
|
|
151
|
-
if (typeof row["provider"] !== "string" || !row["provider"].length) error(path, "has no provider");
|
|
152
|
-
const id = row["id"], provider = row["provider"];
|
|
153
|
-
if (row["disabled"] === true || row["enabled"] === false) { indexed.delete(modelKey({ provider, id })); continue; }
|
|
154
|
-
const previous = indexed.get(modelKey({ provider, id }));
|
|
155
|
-
const model = modelPatch(row, previous, provider, id, path, true);
|
|
156
|
-
const access = accessPatch(row, path, previous ? providerAccess(previous) : {});
|
|
157
|
-
if (Object.keys(access).length) { model.driver = "pi"; setProviderAccess(model, access); }
|
|
158
|
-
indexed.set(modelKey(model), model);
|
|
159
|
-
} catch (err) {
|
|
160
|
-
report(err);
|
|
161
|
-
if (object(row) && typeof row["id"] === "string" && typeof row["provider"] === "string") {
|
|
162
|
-
const key = modelKey({ id: row["id"], provider: row["provider"] });
|
|
163
|
-
const previous = indexed.get(key);
|
|
164
|
-
if (previous) { const copy = { ...previous }; setProviderAccess(copy, { invalid: `${path}: invalid model configuration` }); indexed.set(key, copy); }
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
models = [...indexed.values()];
|
|
169
|
-
for (const groupKey of ["providers", "provider"] as const) {
|
|
170
|
-
const providers = root[groupKey];
|
|
171
|
-
if (providers === undefined) continue;
|
|
172
|
-
if (!object(providers)) { errors.push(`${groupKey}: must be an object`); continue; }
|
|
173
|
-
for (const [provider, config] of Object.entries(providers)) {
|
|
174
|
-
const path = `${groupKey}.${provider}`;
|
|
175
|
-
try {
|
|
176
|
-
if (!object(config)) error(path, "must be an object");
|
|
177
|
-
if (config["disabled"] === true || config["enabled"] === false) { models = models.filter(model => model.provider !== provider); continue; }
|
|
178
|
-
const access = accessPatch(config, path);
|
|
179
|
-
const allow = strings(config["allowlist"] ?? config["whitelist"], `${path}.allowlist`);
|
|
180
|
-
const deny = strings(config["denylist"] ?? config["blacklist"], `${path}.denylist`);
|
|
181
|
-
const peers: ModelSpec[] = [];
|
|
182
|
-
const defaults = { ...config }; delete defaults["name"];
|
|
183
|
-
for (const previous of models.filter(model => model.provider === provider)) {
|
|
184
|
-
const next = modelPatch(defaults, previous, provider, previous.id, path, false);
|
|
185
|
-
setProviderAccess(next, accessPatch(config, path, providerAccess(previous))); peers.push(next);
|
|
186
|
-
}
|
|
187
|
-
const definitions = config["models"];
|
|
188
|
-
if (definitions !== undefined && !Array.isArray(definitions) && !object(definitions)) error(`${path}.models`, "must be an array or object");
|
|
189
|
-
const additions: [string | undefined, unknown][] = Array.isArray(definitions) ? definitions.map(row => [undefined, row]) : object(definitions) ? Object.entries(definitions) : [];
|
|
190
|
-
const patches = config["modelOverrides"];
|
|
191
|
-
if (patches !== undefined && !object(patches)) error(`${path}.modelOverrides`, "must be an object");
|
|
192
|
-
const entries = [...additions.map(([id, row]) => ({ id, row, override: false })), ...Object.entries(object(patches) ? patches : {}).map(([id, row]) => ({ id, row, override: true }))];
|
|
193
|
-
for (const [index, entry] of entries.entries()) {
|
|
194
|
-
const label = `${path}.${entry.override ? "modelOverrides" : "models"}.${entry.id ?? index}`;
|
|
195
|
-
try {
|
|
196
|
-
if (!object(entry.row)) error(label, "must be an object");
|
|
197
|
-
const id = entry.id ?? entry.row["id"];
|
|
198
|
-
if (typeof id !== "string" || !id.length) error(label, "has no id");
|
|
199
|
-
if (entry.row["disabled"] === true || entry.row["enabled"] === false) { const at = peers.findIndex(model => model.id === id); if (at >= 0) peers.splice(at, 1); continue; }
|
|
200
|
-
const base = peers.find(model => model.id === id);
|
|
201
|
-
if (entry.override && !base) error(label, "cannot override an unknown model");
|
|
202
|
-
const inherited = base ?? modelPatch(defaults, undefined, provider, id, path, false);
|
|
203
|
-
const model = modelPatch(entry.row, inherited, provider, id, label, false);
|
|
204
|
-
if (entry.id && typeof entry.row["id"] === "string" && entry.row["id"] !== id) model.wireId = entry.row["id"];
|
|
205
|
-
setProviderAccess(model, accessPatch(entry.row, label, base ? providerAccess(base) : access));
|
|
206
|
-
const at = peers.findIndex(candidate => candidate.id === id);
|
|
207
|
-
if (at < 0) peers.push(model); else peers[at] = model;
|
|
208
|
-
} catch (err) {
|
|
209
|
-
report(err);
|
|
210
|
-
const id = entry.id ?? (object(entry.row) ? entry.row["id"] : undefined);
|
|
211
|
-
const at = peers.findIndex(model => model.id === id);
|
|
212
|
-
if (at >= 0) { const copy = { ...peers[at]! }; setProviderAccess(copy, { invalid: `${label}: invalid model configuration` }); peers[at] = copy; }
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
models = mergeCatalogs(models.filter(model => model.provider !== provider), peers.filter(model => modelEnabled(model.id, allow, deny)));
|
|
216
|
-
} catch (err) {
|
|
217
|
-
report(err);
|
|
218
|
-
// An invalid explicit provider config must not silently use a different credential/endpoint.
|
|
219
|
-
models = models.map(model => {
|
|
220
|
-
if (model.provider !== provider) return model;
|
|
221
|
-
const copy = { ...model }; setProviderAccess(copy, { invalid: `${path}: invalid provider configuration` }); return copy;
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
try {
|
|
227
|
-
const enabled = strings(root["enabled_providers"], "enabled_providers");
|
|
228
|
-
const disabled = strings(root["disabled_providers"], "disabled_providers");
|
|
229
|
-
models = models.filter(model => modelEnabled(model.provider, enabled, disabled));
|
|
230
|
-
} catch (err) { report(err); }
|
|
231
|
-
return { models, errors };
|
|
232
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
The generated model catalog contains data adapted from:
|
|
2
|
-
|
|
3
|
-
Pi (https://github.com/earendil-works/pi)
|
|
4
|
-
Copyright (c) 2025 Mario Zechner
|
|
5
|
-
|
|
6
|
-
Models.dev (https://github.com/anomalyco/models.dev)
|
|
7
|
-
Copyright (c) 2025 models.dev
|
|
8
|
-
|
|
9
|
-
Both sources are distributed under the following MIT License:
|
|
10
|
-
|
|
11
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
-
in the Software without restriction, including without limitation the rights
|
|
14
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
-
furnished to do so, subject to the following conditions:
|
|
17
|
-
|
|
18
|
-
The above copyright notice and this permission notice shall be included in all
|
|
19
|
-
copies or substantial portions of the Software.
|
|
20
|
-
|
|
21
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
-
SOFTWARE.
|