@bitkyc08/opencodex 2.7.43 → 2.8.2-preview.20260731
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/bin/ocx.mjs +34 -8
- package/gui/dist/assets/index-BHsKRFh9.css +1 -0
- package/gui/dist/assets/index-GC0Vlu1Z.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +42 -7
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/kiro.ts +15 -1
- package/src/adapters/openai-chat.ts +55 -4
- package/src/claude/alias.ts +94 -14
- package/src/claude/outbound.ts +6 -3
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/claude.ts +32 -7
- package/src/cli/doctor.ts +48 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/init.ts +129 -102
- package/src/cli/interactive-confirm.ts +5 -1
- package/src/cli/star-prompt.ts +26 -4
- package/src/cli/v2.ts +10 -1
- package/src/codex/account-store.ts +2 -0
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +26 -1
- package/src/codex/catalog/provider-fetch.ts +240 -82
- package/src/codex/catalog/sync.ts +27 -5
- package/src/codex/catalog.ts +3 -3
- package/src/codex/features.ts +524 -5
- package/src/codex/quota.ts +77 -2
- package/src/codex/runtime.ts +10 -1
- package/src/config.ts +8 -0
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +6 -20
- package/src/lib/destination-policy.ts +21 -3
- package/src/lib/provider-outbound.ts +8 -2
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- package/src/lib/winsw.ts +6 -0
- package/src/oauth/index.ts +29 -5
- package/src/oauth/key-providers.ts +21 -2
- package/src/oauth/kiro-credentials.ts +129 -9
- package/src/oauth/kiro.ts +15 -3
- package/src/oauth/login-cli.ts +1 -1
- package/src/oauth/store.ts +2 -0
- package/src/providers/derive.ts +2 -2
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/registry.ts +114 -0
- package/src/router.ts +5 -3
- package/src/server/auth-cors.ts +4 -2
- package/src/server/index.ts +3 -3
- package/src/server/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +82 -8
- package/src/server/management/config-routes.ts +24 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/model-routes.ts +61 -14
- package/src/server/management/provider-routes.ts +44 -9
- package/src/server/management/shared.ts +18 -5
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/proxy-liveness.ts +9 -2
- package/src/server/responses/core.ts +31 -20
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/startup-action-control.ts +30 -14
- package/src/service.ts +395 -31
- package/src/storage/policy-job.ts +26 -5
- package/src/storage/restore-job.ts +16 -5
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +86 -13
- package/src/types.ts +16 -0
- package/src/update/badge.ts +72 -0
- package/src/update/job.ts +8 -4
- package/src/usage/expected-prices.ts +6 -5
- package/src/usage/log.ts +8 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-Czw-jpTU.css +0 -1
- package/gui/dist/assets/index-cmds12BG.js +0 -67
package/src/oauth/store.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
19
19
|
import { chmodSync, closeSync, copyFileSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret } from "../config";
|
|
22
|
+
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
22
23
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
23
24
|
import { validateCopilotApiBaseUrl } from "./github-copilot";
|
|
24
25
|
import type { OAuthCredentialSource, OAuthCredentials, ProviderAccount, ProviderAccountSet } from "./types";
|
|
@@ -131,6 +132,7 @@ export function peekAuthStore(): AuthStore {
|
|
|
131
132
|
|
|
132
133
|
function persist(store: AuthStore): void {
|
|
133
134
|
const dir = getConfigDir();
|
|
135
|
+
assertNotRealHomeUnderTest(dir);
|
|
134
136
|
if (!existsSync(dir)) {
|
|
135
137
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
136
138
|
} else {
|
package/src/providers/derive.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CodexAccountMode, OcxProviderConfig } from "../types";
|
|
2
|
-
import { PROVIDER_REGISTRY, type ProviderRegistryEntry } from "./registry";
|
|
2
|
+
import { PROVIDER_REGISTRY, providerMatchesRegistryTransport, type ProviderRegistryEntry } from "./registry";
|
|
3
3
|
|
|
4
4
|
export interface DerivedKeyLoginProvider {
|
|
5
5
|
label: string;
|
|
@@ -223,7 +223,7 @@ export function deriveProviderPresets(): DerivedProviderPreset[] {
|
|
|
223
223
|
|
|
224
224
|
export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig): void {
|
|
225
225
|
const entry = PROVIDER_REGISTRY.find(row => row.id === name);
|
|
226
|
-
if (!entry) return;
|
|
226
|
+
if (!entry || !providerMatchesRegistryTransport(name, prov)) return;
|
|
227
227
|
const seed = providerConfigSeed(entry);
|
|
228
228
|
if (prov.apiKeyTransport === undefined && seed.apiKeyTransport !== undefined) prov.apiKeyTransport = seed.apiKeyTransport;
|
|
229
229
|
if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
|
|
@@ -18,7 +18,7 @@ export const FREE_PROVIDER_ACCESS_GROUPS = {
|
|
|
18
18
|
],
|
|
19
19
|
"recurring-credit": ["bytez", "nous-research"],
|
|
20
20
|
"signup-credit": [
|
|
21
|
-
"agentrouter", "ai21", "baichuan", "deepinfra", "deepseek", "doubao", "fireworks", "freemodel-dev", "glm-cn",
|
|
21
|
+
"agentrouter", "ai21", "baichuan", "baseten", "deepinfra", "deepseek", "doubao", "fireworks", "freemodel-dev", "glm-cn",
|
|
22
22
|
"hyperbolic", "longcat", "monsterapi", "nebius", "novita", "nscale", "nvidia", "predibase", "publicai", "qoder",
|
|
23
23
|
"scaleway", "sensenova", "stepfun", "together", "vertex",
|
|
24
24
|
],
|
|
@@ -119,6 +119,9 @@ const CONNECTABLE: Record<string, ConnectableOverride> = {
|
|
|
119
119
|
agentrouter: { baseUrl: "https://agentrouter.org", dashboardUrl: "https://agentrouter.org", adapter: "anthropic", authKind: "key", supportLevel: "experimental", verification: "primary", modelsUrl: "https://agentrouter.org/v1/models", lastVerified: LAST_VERIFIED, discovery: "live", liveModels: true },
|
|
120
120
|
ai21: openAi("https://api.ai21.com/studio/v1", "https://studio.ai21.com/account/api-key", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.ai21.com/reference/models" }),
|
|
121
121
|
baichuan: openAi("https://api.baichuan-ai.com/v1", "https://platform.baichuan-ai.com/console/apikey", { verification: "official" }),
|
|
122
|
+
// Verified end-to-end 2026-07-30: /v1/models returns the OpenAI-shaped live catalog (13 models),
|
|
123
|
+
// and a chat completion against moonshotai/Kimi-K3 returned a standard chat.completion payload.
|
|
124
|
+
baseten: openAi("https://inference.baseten.co/v1", "https://app.baseten.co/settings/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.baseten.co/inference/model-apis/overview", modelsUrl: "https://inference.baseten.co/v1/models", lastVerified: "2026-07-30" }),
|
|
122
125
|
deepinfra: openAi("https://api.deepinfra.com/v1/openai", "https://deepinfra.com/dash/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://deepinfra.com/docs/openai_api" }),
|
|
123
126
|
deepseek: openAi("https://api.deepseek.com", "https://platform.deepseek.com/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://api-docs.deepseek.com/api/list-models" }),
|
|
124
127
|
doubao: openAi("https://ark.cn-beijing.volces.com/api/v3", "https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey", { verification: "official" }),
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import type { OcxProviderConfig } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
getProviderRegistryEntry,
|
|
4
|
+
providerMatchesRegistryTransport,
|
|
5
|
+
type ProviderModelDiscoveryFilter,
|
|
6
|
+
type ProviderModelDiscoveryPredicate,
|
|
7
|
+
type ProviderModelDiscoveryScalar,
|
|
8
|
+
type ProviderModelDiscoverySpec,
|
|
9
|
+
} from "./registry";
|
|
10
|
+
|
|
11
|
+
/** Hard process-wide limits. Registry entries may lower, but never raise, these ceilings. */
|
|
12
|
+
export const MODEL_DISCOVERY_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
13
|
+
export const MODEL_DISCOVERY_MAX_MODELS = 2_000;
|
|
14
|
+
export const MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH = 1_024;
|
|
15
|
+
const MODEL_DISCOVERY_MAX_FILTER_VALUES = 256;
|
|
16
|
+
const MODEL_DISCOVERY_MAX_FILTER_STRING_LENGTH = 1_024;
|
|
17
|
+
const MODEL_DISCOVERY_MODEL_ID_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
18
|
+
|
|
19
|
+
export interface ResolvedProviderModelDiscovery {
|
|
20
|
+
spec?: ProviderModelDiscoverySpec;
|
|
21
|
+
maxResponseBytes: number;
|
|
22
|
+
maxModels: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ProviderModelsApiItem = Record<string, unknown> & { id: string };
|
|
26
|
+
|
|
27
|
+
export type ModelDiscoveryResponseFailure =
|
|
28
|
+
| "response_too_large"
|
|
29
|
+
| "invalid_json"
|
|
30
|
+
| "invalid_shape"
|
|
31
|
+
| "too_many_models";
|
|
32
|
+
|
|
33
|
+
export type BoundedDiscoveryJsonResult =
|
|
34
|
+
| { ok: true; value: unknown }
|
|
35
|
+
| { ok: false; reason: "response_too_large" | "invalid_json" };
|
|
36
|
+
|
|
37
|
+
export type ProviderModelItemsResult =
|
|
38
|
+
| { ok: true; items: ProviderModelsApiItem[]; rawCount: number }
|
|
39
|
+
| { ok: false; reason: "invalid_shape" | "too_many_models" };
|
|
40
|
+
|
|
41
|
+
export type ModelEnvelopeRowsResult =
|
|
42
|
+
| { ok: true; rows: unknown[] }
|
|
43
|
+
| { ok: false; reason: "invalid_shape" | "too_many_models" };
|
|
44
|
+
|
|
45
|
+
function positiveIntegerAtMost(value: number | undefined, hardLimit: number): number {
|
|
46
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return hardLimit;
|
|
47
|
+
return Math.min(Math.floor(value), hardLimit);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function discoveryPredicateError(predicate: ProviderModelDiscoveryPredicate): string | null {
|
|
51
|
+
if (!Array.isArray(predicate.path) || predicate.path.length === 0 || predicate.path.length > 8) {
|
|
52
|
+
return "predicate path must contain 1-8 segments";
|
|
53
|
+
}
|
|
54
|
+
if (predicate.path.some(segment => typeof segment !== "string" || !segment.trim() || segment.length > 64)) {
|
|
55
|
+
return "predicate path segments must be nonblank strings up to 64 characters";
|
|
56
|
+
}
|
|
57
|
+
const values = "equalsAny" in predicate
|
|
58
|
+
? predicate.equalsAny
|
|
59
|
+
: "containsAny" in predicate
|
|
60
|
+
? predicate.containsAny
|
|
61
|
+
: predicate.containsAll;
|
|
62
|
+
if (!Array.isArray(values) || values.length === 0 || values.length > 32) {
|
|
63
|
+
return "predicate values must contain 1-32 scalars";
|
|
64
|
+
}
|
|
65
|
+
if (values.some(value => (
|
|
66
|
+
(typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
|
|
67
|
+
|| (typeof value === "string" && (!value.trim() || value.length > 128))
|
|
68
|
+
|| (typeof value === "number" && !Number.isFinite(value))
|
|
69
|
+
))) {
|
|
70
|
+
return "predicate values must be finite booleans/numbers or nonblank strings up to 128 characters";
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Static registry validation used by parity tests; discovery metadata never comes from config. */
|
|
76
|
+
export function providerModelDiscoverySpecError(spec: ProviderModelDiscoverySpec): string | null {
|
|
77
|
+
if (spec.url && spec.path) return "url and path are mutually exclusive";
|
|
78
|
+
if (spec.url !== undefined) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = new URL(spec.url);
|
|
81
|
+
if (parsed.protocol !== "https:") return "absolute discovery url must use https";
|
|
82
|
+
if (parsed.username || parsed.password || parsed.hash) return "absolute discovery url must not contain credentials or a fragment";
|
|
83
|
+
} catch {
|
|
84
|
+
return "absolute discovery url must be valid";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (spec.path !== undefined) {
|
|
88
|
+
const path = spec.path.trim();
|
|
89
|
+
if (!path || path.length > 512) return "discovery path must be 1-512 characters";
|
|
90
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(path) || path.startsWith("//") || path.includes("?") || path.includes("#")) {
|
|
91
|
+
return "discovery path must be a query-free relative/origin path";
|
|
92
|
+
}
|
|
93
|
+
if (path.includes("\\")) return "discovery path must use forward slashes";
|
|
94
|
+
if (path.split("/").some(segment => segment.replace(/%2e/gi, ".") === "..")) {
|
|
95
|
+
return "discovery path must not contain parent-directory segments";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const queryEntries = Object.entries(spec.query ?? {});
|
|
99
|
+
if (queryEntries.length > 32) return "discovery query may contain at most 32 entries";
|
|
100
|
+
if (queryEntries.some(([key, value]) => !key.trim() || key.length > 128 || typeof value !== "string" || value.length > 512)) {
|
|
101
|
+
return "discovery query keys/values exceed their bounds";
|
|
102
|
+
}
|
|
103
|
+
for (const [field, value, hardLimit] of [
|
|
104
|
+
["maxResponseBytes", spec.maxResponseBytes, MODEL_DISCOVERY_MAX_RESPONSE_BYTES],
|
|
105
|
+
["maxModels", spec.maxModels, MODEL_DISCOVERY_MAX_MODELS],
|
|
106
|
+
] as const) {
|
|
107
|
+
if (value !== undefined && (!Number.isInteger(value) || value <= 0 || value > hardLimit)) {
|
|
108
|
+
return `${field} must be a positive integer no greater than ${hardLimit}`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const [group, predicates] of Object.entries(spec.filter ?? {})) {
|
|
112
|
+
if (!Array.isArray(predicates) || predicates.length === 0 || predicates.length > 32) {
|
|
113
|
+
return `${group} must contain 1-32 predicates`;
|
|
114
|
+
}
|
|
115
|
+
for (const predicate of predicates) {
|
|
116
|
+
const error = discoveryPredicateError(predicate);
|
|
117
|
+
if (error) return `${group}: ${error}`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function resolveProviderModelDiscovery(
|
|
124
|
+
providerName: string,
|
|
125
|
+
provider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>,
|
|
126
|
+
): ResolvedProviderModelDiscovery {
|
|
127
|
+
const entry = providerMatchesRegistryTransport(providerName, provider)
|
|
128
|
+
? getProviderRegistryEntry(providerName)
|
|
129
|
+
: undefined;
|
|
130
|
+
const spec = entry?.modelDiscovery;
|
|
131
|
+
return {
|
|
132
|
+
...(spec ? { spec } : {}),
|
|
133
|
+
maxResponseBytes: positiveIntegerAtMost(spec?.maxResponseBytes, MODEL_DISCOVERY_MAX_RESPONSE_BYTES),
|
|
134
|
+
maxModels: positiveIntegerAtMost(spec?.maxModels, MODEL_DISCOVERY_MAX_MODELS),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function appendDiscoveryQuery(url: URL, query: Readonly<Record<string, string>> | undefined): URL {
|
|
139
|
+
for (const [key, value] of Object.entries(query ?? {})) url.searchParams.set(key, value);
|
|
140
|
+
return url;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Apply a registry-owned URL/path/query policy to the adapter's normal discovery endpoint. */
|
|
144
|
+
export function resolveProviderModelDiscoveryUrl(
|
|
145
|
+
providerName: string,
|
|
146
|
+
configuredProvider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>,
|
|
147
|
+
effectiveBaseUrl: string,
|
|
148
|
+
defaultUrl: string,
|
|
149
|
+
): string {
|
|
150
|
+
const { spec } = resolveProviderModelDiscovery(providerName, configuredProvider);
|
|
151
|
+
if (!spec) return defaultUrl;
|
|
152
|
+
|
|
153
|
+
let resolved: URL;
|
|
154
|
+
if (spec.url) {
|
|
155
|
+
resolved = new URL(spec.url);
|
|
156
|
+
} else if (spec.path) {
|
|
157
|
+
const base = new URL(effectiveBaseUrl.endsWith("/") ? effectiveBaseUrl : `${effectiveBaseUrl}/`);
|
|
158
|
+
resolved = spec.path.startsWith("/")
|
|
159
|
+
? new URL(spec.path, base.origin)
|
|
160
|
+
: new URL(spec.path, base);
|
|
161
|
+
} else {
|
|
162
|
+
resolved = new URL(defaultUrl);
|
|
163
|
+
}
|
|
164
|
+
return appendDiscoveryQuery(resolved, spec.query).toString();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function cancelWithoutWaiting(reader: ReadableStreamDefaultReader<Uint8Array>, reason: unknown): void {
|
|
168
|
+
try {
|
|
169
|
+
void reader.cancel(reason).catch(() => undefined);
|
|
170
|
+
} catch {
|
|
171
|
+
// A non-conforming stream may throw synchronously from cancel().
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Read a discovery response under a strict byte ceiling before JSON.parse can allocate freely. */
|
|
176
|
+
export async function readBoundedDiscoveryJson(
|
|
177
|
+
response: Response,
|
|
178
|
+
maxResponseBytes: number,
|
|
179
|
+
): Promise<BoundedDiscoveryJsonResult> {
|
|
180
|
+
const limit = positiveIntegerAtMost(maxResponseBytes, MODEL_DISCOVERY_MAX_RESPONSE_BYTES);
|
|
181
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
182
|
+
if (Number.isFinite(declaredLength) && declaredLength > limit) {
|
|
183
|
+
try {
|
|
184
|
+
void response.body?.cancel(new DOMException("Model discovery response is too large", "QuotaExceededError"))
|
|
185
|
+
.catch(() => undefined);
|
|
186
|
+
} catch {
|
|
187
|
+
// Best-effort cancellation only.
|
|
188
|
+
}
|
|
189
|
+
return { ok: false, reason: "response_too_large" };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!response.body) return { ok: false, reason: "invalid_json" };
|
|
193
|
+
const reader = response.body.getReader();
|
|
194
|
+
const chunks: Uint8Array[] = [];
|
|
195
|
+
let total = 0;
|
|
196
|
+
try {
|
|
197
|
+
while (true) {
|
|
198
|
+
const { value, done } = await reader.read();
|
|
199
|
+
if (done) break;
|
|
200
|
+
if (!value || value.byteLength === 0) continue;
|
|
201
|
+
if (value.byteLength > limit - total) {
|
|
202
|
+
cancelWithoutWaiting(
|
|
203
|
+
reader,
|
|
204
|
+
new DOMException("Model discovery response is too large", "QuotaExceededError"),
|
|
205
|
+
);
|
|
206
|
+
return { ok: false, reason: "response_too_large" };
|
|
207
|
+
}
|
|
208
|
+
chunks.push(value);
|
|
209
|
+
total += value.byteLength;
|
|
210
|
+
}
|
|
211
|
+
} finally {
|
|
212
|
+
try {
|
|
213
|
+
reader.releaseLock();
|
|
214
|
+
} catch {
|
|
215
|
+
// Cancellation may keep the lock briefly.
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const bytes = new Uint8Array(total);
|
|
220
|
+
let offset = 0;
|
|
221
|
+
for (const chunk of chunks) {
|
|
222
|
+
bytes.set(chunk, offset);
|
|
223
|
+
offset += chunk.byteLength;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
227
|
+
return { ok: true, value: JSON.parse(text) as unknown };
|
|
228
|
+
} catch {
|
|
229
|
+
return { ok: false, reason: "invalid_json" };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function valueAtPath(item: Record<string, unknown>, path: readonly string[]): unknown {
|
|
234
|
+
let current: unknown = item;
|
|
235
|
+
for (const segment of path) {
|
|
236
|
+
if (current === null || typeof current !== "object" || Array.isArray(current)) return undefined;
|
|
237
|
+
current = (current as Record<string, unknown>)[segment];
|
|
238
|
+
}
|
|
239
|
+
return current;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function comparableScalar(value: unknown, caseInsensitive: boolean): ProviderModelDiscoveryScalar | undefined {
|
|
243
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return undefined;
|
|
244
|
+
if (typeof value === "string" && value.length > MODEL_DISCOVERY_MAX_FILTER_STRING_LENGTH) return undefined;
|
|
245
|
+
return caseInsensitive && typeof value === "string" ? value.toLowerCase() : value;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function comparableNeedles(
|
|
249
|
+
values: readonly ProviderModelDiscoveryScalar[],
|
|
250
|
+
caseInsensitive: boolean,
|
|
251
|
+
): ProviderModelDiscoveryScalar[] {
|
|
252
|
+
return values.map(value => caseInsensitive && typeof value === "string" ? value.toLowerCase() : value);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function predicateMatches(item: ProviderModelsApiItem, predicate: ProviderModelDiscoveryPredicate): boolean {
|
|
256
|
+
const caseInsensitive = predicate.caseInsensitive === true;
|
|
257
|
+
const raw = valueAtPath(item, predicate.path);
|
|
258
|
+
if ("equalsAny" in predicate) {
|
|
259
|
+
const value = comparableScalar(raw, caseInsensitive);
|
|
260
|
+
return value !== undefined && comparableNeedles(predicate.equalsAny, caseInsensitive).includes(value);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const collection = Array.isArray(raw);
|
|
264
|
+
const values: ProviderModelDiscoveryScalar[] = [];
|
|
265
|
+
if (collection) {
|
|
266
|
+
for (let i = 0; i < raw.length && i < MODEL_DISCOVERY_MAX_FILTER_VALUES; i += 1) {
|
|
267
|
+
const value = comparableScalar(raw[i], caseInsensitive);
|
|
268
|
+
if (value !== undefined) values.push(value);
|
|
269
|
+
}
|
|
270
|
+
} else if (typeof raw === "string") {
|
|
271
|
+
values.push(caseInsensitive ? raw.toLowerCase() : raw);
|
|
272
|
+
}
|
|
273
|
+
const needles = comparableNeedles(
|
|
274
|
+
"containsAny" in predicate ? predicate.containsAny : predicate.containsAll,
|
|
275
|
+
caseInsensitive,
|
|
276
|
+
);
|
|
277
|
+
if ("containsAny" in predicate) {
|
|
278
|
+
return needles.some(needle => values.some(value => (
|
|
279
|
+
!collection && typeof value === "string" && typeof needle === "string" ? value.includes(needle) : value === needle
|
|
280
|
+
)));
|
|
281
|
+
}
|
|
282
|
+
return needles.every(needle => values.some(value => (
|
|
283
|
+
!collection && typeof value === "string" && typeof needle === "string" ? value.includes(needle) : value === needle
|
|
284
|
+
)));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function providerModelMatchesDiscoveryFilter(
|
|
288
|
+
item: ProviderModelsApiItem,
|
|
289
|
+
filter: ProviderModelDiscoveryFilter | undefined,
|
|
290
|
+
): boolean {
|
|
291
|
+
if (!filter) return true;
|
|
292
|
+
if (filter.allOf && !filter.allOf.every(predicate => predicateMatches(item, predicate))) return false;
|
|
293
|
+
if (filter.anyOf && filter.anyOf.length > 0 && !filter.anyOf.some(predicate => predicateMatches(item, predicate))) return false;
|
|
294
|
+
if (filter.noneOf?.some(predicate => predicateMatches(item, predicate))) return false;
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Extract one allowlisted array envelope while enforcing the raw-row ceiling. */
|
|
299
|
+
export function extractModelEnvelopeRows(
|
|
300
|
+
value: unknown,
|
|
301
|
+
maxModels: number,
|
|
302
|
+
envelopeKeys: readonly string[],
|
|
303
|
+
): ModelEnvelopeRowsResult {
|
|
304
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
305
|
+
return { ok: false, reason: "invalid_shape" };
|
|
306
|
+
}
|
|
307
|
+
const record = value as Record<string, unknown>;
|
|
308
|
+
const rows = envelopeKeys.map(key => record[key]).find(Array.isArray);
|
|
309
|
+
if (!rows) return { ok: false, reason: "invalid_shape" };
|
|
310
|
+
const limit = positiveIntegerAtMost(maxModels, MODEL_DISCOVERY_MAX_MODELS);
|
|
311
|
+
if (rows.length > limit) return { ok: false, reason: "too_many_models" };
|
|
312
|
+
return { ok: true, rows };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Validate, bound, deduplicate, and declaratively filter OpenAI `{data:[...]}` or top-level arrays (Together `#617`). */
|
|
316
|
+
export function extractProviderModelItems(
|
|
317
|
+
value: unknown,
|
|
318
|
+
discovery: ResolvedProviderModelDiscovery,
|
|
319
|
+
): ProviderModelItemsResult {
|
|
320
|
+
const limit = positiveIntegerAtMost(discovery.maxModels, MODEL_DISCOVERY_MAX_MODELS);
|
|
321
|
+
let data: unknown[];
|
|
322
|
+
if (Array.isArray(value)) {
|
|
323
|
+
// Together-style top-level /models arrays. Catalog discovery must not treat a stray
|
|
324
|
+
// `models` key on openai-chat responses as valid — only `data` envelopes or top-level arrays.
|
|
325
|
+
if (value.length > limit) return { ok: false, reason: "too_many_models" };
|
|
326
|
+
data = value;
|
|
327
|
+
} else {
|
|
328
|
+
const envelope = extractModelEnvelopeRows(value, discovery.maxModels, ["data"]);
|
|
329
|
+
if (!envelope.ok) return envelope;
|
|
330
|
+
data = envelope.rows;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const items: ProviderModelsApiItem[] = [];
|
|
334
|
+
const seen = new Set<string>();
|
|
335
|
+
for (const raw of data) {
|
|
336
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
337
|
+
return { ok: false, reason: "invalid_shape" };
|
|
338
|
+
}
|
|
339
|
+
const id = (raw as { id?: unknown }).id;
|
|
340
|
+
if (typeof id !== "string") return { ok: false, reason: "invalid_shape" };
|
|
341
|
+
const normalizedId = id.trim();
|
|
342
|
+
if (
|
|
343
|
+
!normalizedId
|
|
344
|
+
|| normalizedId !== id
|
|
345
|
+
|| normalizedId.length > MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH
|
|
346
|
+
|| MODEL_DISCOVERY_MODEL_ID_CONTROL_CHARS.test(normalizedId)
|
|
347
|
+
) {
|
|
348
|
+
return { ok: false, reason: "invalid_shape" };
|
|
349
|
+
}
|
|
350
|
+
const item = raw as ProviderModelsApiItem;
|
|
351
|
+
if (!providerModelMatchesDiscoveryFilter(item, discovery.spec?.filter) || seen.has(normalizedId)) continue;
|
|
352
|
+
seen.add(normalizedId);
|
|
353
|
+
items.push(item);
|
|
354
|
+
}
|
|
355
|
+
return { ok: true, items, rawCount: data.length };
|
|
356
|
+
}
|
|
@@ -17,6 +17,73 @@ import {
|
|
|
17
17
|
export type ProviderAuthKind = "forward" | "oauth" | "key" | "local";
|
|
18
18
|
export type MetadataModelIdNormalize = "case-insensitive";
|
|
19
19
|
|
|
20
|
+
export type ProviderModelDiscoveryScalar = string | number | boolean;
|
|
21
|
+
|
|
22
|
+
export type ProviderModelDiscoveryPredicate =
|
|
23
|
+
| {
|
|
24
|
+
path: readonly string[];
|
|
25
|
+
equalsAny: readonly ProviderModelDiscoveryScalar[];
|
|
26
|
+
caseInsensitive?: boolean;
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
path: readonly string[];
|
|
30
|
+
/**
|
|
31
|
+
* A string-valued upstream target uses substring matching; an array-valued target uses
|
|
32
|
+
* exact element matching. Use `equalsAny` when the string must match in full.
|
|
33
|
+
*/
|
|
34
|
+
containsAny: readonly ProviderModelDiscoveryScalar[];
|
|
35
|
+
caseInsensitive?: boolean;
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
path: readonly string[];
|
|
39
|
+
/** Uses the same string-substring and array-element semantics as `containsAny`. */
|
|
40
|
+
containsAll: readonly ProviderModelDiscoveryScalar[];
|
|
41
|
+
caseInsensitive?: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export interface ProviderModelDiscoveryFilter {
|
|
45
|
+
/** Every predicate must match. */
|
|
46
|
+
allOf?: readonly ProviderModelDiscoveryPredicate[];
|
|
47
|
+
/** At least one predicate must match. */
|
|
48
|
+
anyOf?: readonly ProviderModelDiscoveryPredicate[];
|
|
49
|
+
/** No predicate may match. */
|
|
50
|
+
noneOf?: readonly ProviderModelDiscoveryPredicate[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ProviderModelDiscoverySharedSpec {
|
|
54
|
+
/** Query parameters applied to the resolved discovery URL. */
|
|
55
|
+
query?: Readonly<Record<string, string>>;
|
|
56
|
+
/** Declarative eligibility rules evaluated against each untrusted model row. */
|
|
57
|
+
filter?: ProviderModelDiscoveryFilter;
|
|
58
|
+
/** Optional lower byte ceiling; the process-wide hard ceiling still wins. */
|
|
59
|
+
maxResponseBytes?: number;
|
|
60
|
+
/** Optional lower raw-row ceiling; the process-wide hard ceiling still wins. */
|
|
61
|
+
maxModels?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type ProviderModelDiscoveryLocation =
|
|
65
|
+
| {
|
|
66
|
+
/** Registry-owned absolute endpoint. Mutually exclusive with `path`. */
|
|
67
|
+
url: string;
|
|
68
|
+
path?: never;
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
/** Resource path relative to baseUrl; query strings and fragments are disallowed. */
|
|
72
|
+
path: string;
|
|
73
|
+
url?: never;
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
/** Keep the adapter-derived default discovery endpoint. */
|
|
77
|
+
url?: never;
|
|
78
|
+
path?: never;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Trusted live-model discovery policy. This metadata is registry-only: it must never be copied
|
|
83
|
+
* into config.json, where a same-named custom provider could otherwise redirect a stored key.
|
|
84
|
+
*/
|
|
85
|
+
export type ProviderModelDiscoverySpec = ProviderModelDiscoverySharedSpec & ProviderModelDiscoveryLocation;
|
|
86
|
+
|
|
20
87
|
export interface ProviderRegistryEntry {
|
|
21
88
|
id: string;
|
|
22
89
|
label: string;
|
|
@@ -35,6 +102,11 @@ export interface ProviderRegistryEntry {
|
|
|
35
102
|
*/
|
|
36
103
|
freeTier?: boolean;
|
|
37
104
|
allowBaseUrlOverride?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Do not claim an existing same-named key provider whose fixed destination differs from this
|
|
107
|
+
* preset. Enable for newly promoted ids so an older custom key cannot be silently retargeted.
|
|
108
|
+
*/
|
|
109
|
+
preserveCustomDestination?: boolean;
|
|
38
110
|
/**
|
|
39
111
|
* Optional endpoint picker for providers with multiple official hosts
|
|
40
112
|
* (e.g. Qwen Cloud token plan vs pay-as-you-go). Requires `allowBaseUrlOverride`
|
|
@@ -51,6 +123,7 @@ export interface ProviderRegistryEntry {
|
|
|
51
123
|
defaultModel?: string;
|
|
52
124
|
models?: string[];
|
|
53
125
|
liveModels?: boolean;
|
|
126
|
+
modelDiscovery?: ProviderModelDiscoverySpec;
|
|
54
127
|
contextWindow?: number;
|
|
55
128
|
modelContextWindows?: Record<string, number>;
|
|
56
129
|
modelInputModalities?: Record<string, string[]>;
|
|
@@ -384,6 +457,12 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
384
457
|
modelContextWindows: cursorModelContextWindows(CURSOR_STATIC_MODELS),
|
|
385
458
|
modelInputModalities: cursorModelInputModalities(CURSOR_STATIC_MODELS),
|
|
386
459
|
modelReasoningEfforts: cursorModelReasoningEfforts(CURSOR_STATIC_MODELS),
|
|
460
|
+
// Kimi K3 documents `max` as its API default, and its Cursor ladder has no `medium`
|
|
461
|
+
// rung — so applyReasoningLevels' medium->high->first fallback would settle the catalog
|
|
462
|
+
// default on `high`, the picker would send `high` explicitly, and the request builder's
|
|
463
|
+
// no-effort fallback to `kimi-k3-max` would never be reached. Mirrors the other K3
|
|
464
|
+
// routes (kimi, kimi-code, opencode-go).
|
|
465
|
+
modelDefaultReasoningEfforts: { "kimi-k3": "max" },
|
|
387
466
|
// Cursor's wire protocol never forwards image parts (request-builder emits an unsupported-
|
|
388
467
|
// content marker), so the vision sidecar covers ALL cursor models regardless of what the
|
|
389
468
|
// upstream model could natively do. Live-discovered models outside the static list fall back
|
|
@@ -1121,6 +1200,41 @@ export function getProviderRegistryEntry(id: string): ProviderRegistryEntry | un
|
|
|
1121
1200
|
return PROVIDER_REGISTRY.find(entry => entry.id === id);
|
|
1122
1201
|
}
|
|
1123
1202
|
|
|
1203
|
+
function normalizedProviderEndpoint(value: string): string {
|
|
1204
|
+
const trimmed = value.trim();
|
|
1205
|
+
try {
|
|
1206
|
+
const parsed = new URL(trimmed);
|
|
1207
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, "") || "/";
|
|
1208
|
+
return parsed.toString().replace(/\/$/, "");
|
|
1209
|
+
} catch {
|
|
1210
|
+
return trimmed.replace(/\/+$/, "");
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Whether registry transport defaults own this configured row.
|
|
1216
|
+
*
|
|
1217
|
+
* OAuth/forward providers stay pinned because their credentials must never be sent to an
|
|
1218
|
+
* arbitrary same-named host. Existing key presets keep their historical pinning behavior; a new
|
|
1219
|
+
* preset can opt into collision preservation, in which case its fixed endpoint owns only rows
|
|
1220
|
+
* that still match that destination.
|
|
1221
|
+
*/
|
|
1222
|
+
export function providerMatchesRegistryTransport(
|
|
1223
|
+
id: string,
|
|
1224
|
+
provider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>,
|
|
1225
|
+
): boolean {
|
|
1226
|
+
const entry = getProviderRegistryEntry(id);
|
|
1227
|
+
if (!entry) return false;
|
|
1228
|
+
if (entry.authKind !== "key" || entry.preserveCustomDestination !== true) return true;
|
|
1229
|
+
// The opt-in is intentionally limited to fixed key destinations. Fail closed if a future
|
|
1230
|
+
// registry edit combines it with an override/template despite the registry parity tests.
|
|
1231
|
+
if (entry.allowBaseUrlOverride || /\{[^}]*\}/.test(entry.baseUrl)) return false;
|
|
1232
|
+
if (typeof provider.baseUrl !== "string") return false;
|
|
1233
|
+
if (provider.adapter !== entry.adapter) return false;
|
|
1234
|
+
if (provider.authMode !== undefined && provider.authMode !== "key") return false;
|
|
1235
|
+
return normalizedProviderEndpoint(provider.baseUrl) === normalizedProviderEndpoint(entry.baseUrl);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1124
1238
|
/**
|
|
1125
1239
|
* Effective Codex account mode for a provider. For canonical `openai`, a valid persisted
|
|
1126
1240
|
* `codexAccountMode` on the provider config wins and a missing/invalid value defaults to
|
package/src/router.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { preservesPhysicalComboProvider, tryPickComboModel, type ComboPick } fro
|
|
|
3
3
|
import { hasOwnProvider, resolveEnvValue } from "./config";
|
|
4
4
|
import { assertProviderDestinationAllowed } from "./lib/destination-policy";
|
|
5
5
|
import { redactSecretString, redactUrlForLog } from "./lib/redact";
|
|
6
|
-
import { PROVIDER_REGISTRY, providerCodexAccountMode } from "./providers/registry";
|
|
6
|
+
import { PROVIDER_REGISTRY, providerCodexAccountMode, providerMatchesRegistryTransport } from "./providers/registry";
|
|
7
7
|
import { LEGACY_CHATGPT_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "./providers/openai-tiers";
|
|
8
8
|
import { decodeRoutedModelId, encodeRoutedModelId } from "./providers/slug-codec";
|
|
9
9
|
import { getStaleCached } from "./codex/model-cache";
|
|
@@ -40,7 +40,9 @@ const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string
|
|
|
40
40
|
export function knownModelIdsForProvider(provName: string, prov: OcxProviderConfig): string[] {
|
|
41
41
|
const ids = new Set<string>();
|
|
42
42
|
for (const id of prov.models ?? []) ids.add(id);
|
|
43
|
-
const registry =
|
|
43
|
+
const registry = providerMatchesRegistryTransport(provName, prov)
|
|
44
|
+
? PROVIDER_REGISTRY.find(entry => entry.id === provName)
|
|
45
|
+
: undefined;
|
|
44
46
|
for (const id of registry?.models ?? []) ids.add(id);
|
|
45
47
|
// Registry model-keyed hint maps double as known native ids (e.g. NVIDIA carries no
|
|
46
48
|
// static models list but names `moonshotai/kimi-k2.6` in its effort/window maps).
|
|
@@ -192,7 +194,7 @@ function usableResolvedApiKey(apiKey: string | undefined): string | undefined {
|
|
|
192
194
|
|
|
193
195
|
function routedProviderConfig(providerName: string, provider: OcxProviderConfig): OcxProviderConfig {
|
|
194
196
|
const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
|
|
195
|
-
if (!registryEntry) {
|
|
197
|
+
if (!registryEntry || !providerMatchesRegistryTransport(providerName, provider)) {
|
|
196
198
|
assertProviderDestinationAllowed(providerName, provider);
|
|
197
199
|
return { ...provider, apiKey: usableResolvedApiKey(provider.apiKey) };
|
|
198
200
|
}
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
reasoningSummaryDeliveryRecordConfigError,
|
|
13
13
|
} from "../config";
|
|
14
14
|
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
15
|
-
import { getProviderRegistryEntry, providerCodexAccountMode } from "../providers/registry";
|
|
15
|
+
import { getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport } from "../providers/registry";
|
|
16
16
|
import { providerConfigSeed } from "../providers/derive";
|
|
17
17
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
18
18
|
import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
|
|
@@ -418,7 +418,9 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
418
418
|
] as const) {
|
|
419
419
|
copyIfDefined(dto, provider, key);
|
|
420
420
|
}
|
|
421
|
-
const registryNote =
|
|
421
|
+
const registryNote = providerMatchesRegistryTransport(name, provider)
|
|
422
|
+
? getProviderRegistryEntry(name)?.note
|
|
423
|
+
: undefined;
|
|
422
424
|
if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
|
|
423
425
|
const codexAccountMode = providerCodexAccountMode(name, provider);
|
|
424
426
|
if (codexAccountMode) dto.codexAccountMode = codexAccountMode;
|
package/src/server/index.ts
CHANGED
|
@@ -406,7 +406,7 @@ export function startServer(port?: number) {
|
|
|
406
406
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
407
407
|
}
|
|
408
408
|
const goModels = await fetchAllModels(config);
|
|
409
|
-
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs } = await import("../codex/catalog");
|
|
409
|
+
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
410
410
|
const nativeSlugs = nativeOpenAiSlugs();
|
|
411
411
|
const goEnabled = filterCatalogVisibleModels(goModels, config);
|
|
412
412
|
const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
|
|
@@ -424,7 +424,7 @@ export function startServer(port?: number) {
|
|
|
424
424
|
if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, config);
|
|
425
425
|
// Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
|
|
426
426
|
buildDesktop3pRegistry(
|
|
427
|
-
[...
|
|
427
|
+
[...desktopVisibleNativeSlugs(config)],
|
|
428
428
|
goOrdered.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
|
|
429
429
|
config.claudeCode?.desktopProfile,
|
|
430
430
|
);
|
|
@@ -441,7 +441,7 @@ export function startServer(port?: number) {
|
|
|
441
441
|
: idsParam === "desktop"
|
|
442
442
|
? "desktop3p" as const
|
|
443
443
|
: (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
|
|
444
|
-
const data = buildAnthropicModelInfos([...
|
|
444
|
+
const data = buildAnthropicModelInfos([...desktopVisibleNativeSlugs(config)], goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias);
|
|
445
445
|
return jsonResponse({ data }, 200, req, config);
|
|
446
446
|
}
|
|
447
447
|
if (url.searchParams.has("client_version")) {
|