lynkr 9.14.10 → 9.14.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/bin/cli.js +1 -0
- package/bin/lynkr-connect-orcarouter.js +246 -0
- package/bin/lynkr-init.js +15 -1
- package/config/model-capability-seeds.json +32 -4
- package/package.json +1 -1
- package/src/api/middleware/session.js +10 -1
- package/src/api/openai-router.js +22 -4
- package/src/api/providers-handler.js +33 -10
- package/src/cache/prompt.js +40 -1
- package/src/cache/semantic.js +7 -0
- package/src/clients/databricks.js +219 -2
- package/src/clients/openrouter-utils.js +84 -14
- package/src/clients/orcarouter-catalog.js +330 -0
- package/src/clients/orcarouter-credentials.js +428 -0
- package/src/clients/provider-capabilities.js +1 -1
- package/src/config/index.js +63 -2
- package/src/dashboard/api.js +1 -0
- package/src/logger/audit-logger.js +6 -0
- package/src/logger/deduplicator.js +10 -0
- package/src/orchestrator/index.js +28 -7
- package/src/orchestrator/sse-transformer.js +3 -0
- package/src/routing/cache-economics.js +1 -0
- package/src/routing/client-profiles.js +19 -0
- package/src/routing/index.js +45 -19
- package/src/routing/model-tiers.js +12 -11
- package/src/routing/risk-classifier.js +13 -1
- package/src/routing/tokenizer.js +22 -5
- package/src/routing/vision.js +246 -0
- package/src/training/trajectory-compressor.js +8 -0
- package/src/utils/tokens.js +3 -3
package/README.md
CHANGED
|
@@ -211,6 +211,7 @@ Claude Code / Cursor / Codex / Cline / Continue
|
|
|
211
211
|
| **llama.cpp** | Local | Any GGUF model | **Free** |
|
|
212
212
|
| **LM Studio** | Local | Local models with GUI | **Free** |
|
|
213
213
|
| **OpenRouter** | Cloud | GPT-4o, Claude 3.5, Llama 3, Gemini | $ |
|
|
214
|
+
| **OrcaRouter** | Cloud | Chat, reasoning, image, video (one endpoint, live catalog) | $ (zero-markup) |
|
|
214
215
|
| **AWS Bedrock** | Cloud | Claude, Llama, Mistral, Titan | $$ |
|
|
215
216
|
| **Databricks** | Cloud | Claude Sonnet 4.5, Opus 4.6 | $$$ |
|
|
216
217
|
| **Azure OpenAI** | Cloud | GPT-4o, o1, o3 | $$$ |
|
package/bin/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ const SUBCOMMANDS = {
|
|
|
15
15
|
"desktop-token": path.join(__dirname, "lynkr-desktop-token.js"),
|
|
16
16
|
restart: path.join(__dirname, "lynkr-restart.js"),
|
|
17
17
|
run: path.join(__dirname, "run.js"),
|
|
18
|
+
connect: path.join(__dirname, "lynkr-connect-orcarouter.js"),
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
const sub = process.argv[2];
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `lynkr connect orcarouter` — OAuth 2.0 + PKCE (Flow B, out-of-band) login.
|
|
4
|
+
*
|
|
5
|
+
* Lynkr is self-hosted software whose install address differs on every
|
|
6
|
+
* deployment (LAN box, NAS, moved port), so there is no predictable loopback
|
|
7
|
+
* port to register — Flow B (out-of-band code) exists precisely for that
|
|
8
|
+
* case. The command:
|
|
9
|
+
*
|
|
10
|
+
* 1. Generates a fresh PKCE verifier + CSRF state (crypto RNG, per attempt).
|
|
11
|
+
* 2. Prints an authorize URL (callback_url=oob, S256 challenge) and opens
|
|
12
|
+
* it in the default browser when possible.
|
|
13
|
+
* 3. Asks for the one-time code shown on the consent screen.
|
|
14
|
+
* 4. Exchanges it at www.orcarouter.ai/api/v1/auth/keys for a durable
|
|
15
|
+
* OrcaRouter API key that belongs to the user.
|
|
16
|
+
* 5. Persists the key to the project's .env (same secret location as every
|
|
17
|
+
* other Lynkr provider key) and prints the masked status.
|
|
18
|
+
*
|
|
19
|
+
* The verifier never leaves the process until the exchange; it never appears
|
|
20
|
+
* in a URL, a log, or an error. Auth requests hit the auth origin
|
|
21
|
+
* (www.orcarouter.ai) only; inference stays on api.orcarouter.ai/v1.
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* lynkr connect orcarouter # interactive OOB code login
|
|
25
|
+
* lynkr connect orcarouter --code <code> # non-interactive (CI / GUI)
|
|
26
|
+
* lynkr connect orcarouter --status # masked status of stored key
|
|
27
|
+
* lynkr connect orcarouter --clear # remove the stored key
|
|
28
|
+
*
|
|
29
|
+
* @module bin/lynkr-connect-orcarouter
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const path = require("path");
|
|
33
|
+
const fs = require("fs");
|
|
34
|
+
const readline = require("readline");
|
|
35
|
+
const { exec } = require("child_process");
|
|
36
|
+
|
|
37
|
+
const ROOT = path.join(__dirname, "..");
|
|
38
|
+
const ENV_PATH = path.join(ROOT, ".env");
|
|
39
|
+
const {
|
|
40
|
+
connectWithPkce,
|
|
41
|
+
getCredentialStore,
|
|
42
|
+
createApiKeyAdapter,
|
|
43
|
+
maskKey,
|
|
44
|
+
resolveAuthBase,
|
|
45
|
+
resolveApiBase,
|
|
46
|
+
} = require("../src/clients/orcarouter-credentials");
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// .env persistence — the project's existing secret location. No new secret
|
|
50
|
+
// store is introduced. Keys are written as `ORCAROUTER_API_KEY=...` alongside
|
|
51
|
+
// the other provider keys; existing keys are updated in place.
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
function readEnvLines() {
|
|
55
|
+
try {
|
|
56
|
+
return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split(/\r?\n/) : [];
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeEnvLines(lines) {
|
|
63
|
+
fs.writeFileSync(ENV_PATH, lines.join("\n") + "\n", { mode: 0o600 });
|
|
64
|
+
// mode only applies on creation — tighten existing files explicitly so a
|
|
65
|
+
// previously 0644 .env doesn't leave the new OAuth key world-readable.
|
|
66
|
+
try {
|
|
67
|
+
fs.chmodSync(ENV_PATH, 0o600);
|
|
68
|
+
} catch {
|
|
69
|
+
/* best-effort: e.g. read-only FS in CI */
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readEnvKey(key) {
|
|
74
|
+
try {
|
|
75
|
+
const lines = readEnvLines();
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
const m = line.match(/^([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
|
|
78
|
+
if (m && m[1] === key) {
|
|
79
|
+
const v = (m[2] || "").trim();
|
|
80
|
+
return v || null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
/* missing/unreadable .env → no file-backed value */
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// File-backed env for status: process.env wins, .env file is the fallback so
|
|
90
|
+
// a key saved by a previous `connect` process is visible without re-export.
|
|
91
|
+
function statusEnv() {
|
|
92
|
+
const env = { ...process.env };
|
|
93
|
+
for (const k of ["ORCAROUTER_API_KEY", "ORCA_API_BASE_URL", "ORCA_AUTH_BASE_URL", "ORCA_BASE_URL"]) {
|
|
94
|
+
if (!env[k]) {
|
|
95
|
+
const v = readEnvKey(k);
|
|
96
|
+
if (v) env[k] = v;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return env;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function upsertEnvKey(key, value) {
|
|
103
|
+
const lines = readEnvLines();
|
|
104
|
+
let replaced = false;
|
|
105
|
+
const out = lines.map((line) => {
|
|
106
|
+
const m = line.match(/^([A-Z0-9_]+)\s*=/);
|
|
107
|
+
if (m && m[1] === key) {
|
|
108
|
+
replaced = true;
|
|
109
|
+
return `${key}=${value}`;
|
|
110
|
+
}
|
|
111
|
+
return line;
|
|
112
|
+
});
|
|
113
|
+
if (!replaced) out.push(`${key}=${value}`);
|
|
114
|
+
writeEnvLines(out);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function clearEnvKey(_key) {
|
|
118
|
+
const lines = readEnvLines().filter((line) => !line.match(/^ORCAROUTER_API_KEY\s*=/));
|
|
119
|
+
writeEnvLines(lines);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// (readEnvKey is intentionally not exported — the store's read() covers status.)
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// CLI
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
function openBrowser(url) {
|
|
129
|
+
const platform = process.platform;
|
|
130
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
131
|
+
exec(`"${cmd}" "${url}"`, () => {});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ask(question) {
|
|
135
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
136
|
+
return new Promise((resolve) =>
|
|
137
|
+
rl.question(question, (answer) => {
|
|
138
|
+
rl.close();
|
|
139
|
+
resolve(answer.trim());
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function fail(msg) {
|
|
145
|
+
console.error(`✗ ${msg}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function status() {
|
|
150
|
+
// Read through a file-backed store: a key saved to .env by either
|
|
151
|
+
// connection path must show up in a later process without requiring export.
|
|
152
|
+
const { OrcaCredentialStore } = require("../src/clients/orcarouter-credentials");
|
|
153
|
+
const env = statusEnv();
|
|
154
|
+
const store = new OrcaCredentialStore({
|
|
155
|
+
env,
|
|
156
|
+
loadSecret: () => env.ORCAROUTER_API_KEY || null,
|
|
157
|
+
});
|
|
158
|
+
const st = store.read();
|
|
159
|
+
console.log(st.present
|
|
160
|
+
? `OrcaRouter credential: ${st.masked} (generation ${st.generation})${st.needsReauth ? " — needs reauthentication" : ""}`
|
|
161
|
+
: "No OrcaRouter credential configured.");
|
|
162
|
+
console.log(` API base: ${resolveApiBase(env)}`);
|
|
163
|
+
console.log(` Auth base: ${resolveAuthBase(env)}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function runConnect(opts) {
|
|
167
|
+
const store = getCredentialStore();
|
|
168
|
+
|
|
169
|
+
// Adapter A: update/clear an API key directly through the seam too.
|
|
170
|
+
if (opts.apiKey) {
|
|
171
|
+
const adapter = createApiKeyAdapter(store, { env: process.env });
|
|
172
|
+
const cred = adapter.set(opts.apiKey);
|
|
173
|
+
upsertEnvKey("ORCAROUTER_API_KEY", cred.key);
|
|
174
|
+
console.log(`✓ OrcaRouter API key saved (${maskKey(cred.key)}).`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (opts.clear) {
|
|
178
|
+
store.clear();
|
|
179
|
+
clearEnvKey("ORCAROUTER_API_KEY");
|
|
180
|
+
console.log("✓ OrcaRouter credential cleared.");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (opts.status) {
|
|
184
|
+
status();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// PKCE connect. Generate verifier+state fresh; hand back the authorize URL.
|
|
189
|
+
const started = await connectWithPkce({ appName: "Lynkr" });
|
|
190
|
+
if (!started.ok) fail(started.error?.message || "Could not start OrcaRouter login");
|
|
191
|
+
|
|
192
|
+
console.log("\n1. Open this URL in your browser (opening automatically…):");
|
|
193
|
+
console.log(` ${started.authorizeUrl}\n`);
|
|
194
|
+
openBrowser(started.authorizeUrl);
|
|
195
|
+
|
|
196
|
+
const code = opts.code || (await ask("2. Paste the one-time code from the consent screen: ")).trim();
|
|
197
|
+
if (!code) fail("No code provided.");
|
|
198
|
+
|
|
199
|
+
// Exchange the code for the durable key using the SAME verifier that
|
|
200
|
+
// produced the challenge (PKCE binding — a fresh verifier would mismatch).
|
|
201
|
+
const result = await connectWithPkce({ appName: "Lynkr", code, verifier: started.verifier, state: started.state });
|
|
202
|
+
if (!result.ok) {
|
|
203
|
+
fail(result.error?.message || `OrcaRouter exchange failed (HTTP ${result.error?.status || "?"})`);
|
|
204
|
+
}
|
|
205
|
+
upsertEnvKey("ORCAROUTER_API_KEY", result.credential.key);
|
|
206
|
+
const scopeNote = result.credential.scope && result.credential.scope !== "api"
|
|
207
|
+
? ` (granted scope: ${result.credential.scope})`
|
|
208
|
+
: "";
|
|
209
|
+
console.log(`\n✓ Connected to OrcaRouter. Key saved to .env${scopeNote}.`);
|
|
210
|
+
console.log(` Masked: ${maskKey(result.credential.key)}`);
|
|
211
|
+
console.log(" Revoke anytime at https://www.orcarouter.ai/console/authorized-apps");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseArgs(argv) {
|
|
215
|
+
const opts = { apiKey: null, code: null, status: false, clear: false };
|
|
216
|
+
for (let i = 0; i < argv.length; i++) {
|
|
217
|
+
const a = argv[i];
|
|
218
|
+
if (a === "--status") opts.status = true;
|
|
219
|
+
else if (a === "--clear") opts.clear = true;
|
|
220
|
+
else if (a === "--api-key") opts.apiKey = argv[++i];
|
|
221
|
+
else if (a === "--code") opts.code = argv[++i];
|
|
222
|
+
}
|
|
223
|
+
return opts;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function main() {
|
|
227
|
+
const args = process.argv.slice(2);
|
|
228
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
229
|
+
console.log(`lynkr connect orcarouter — OAuth 2.0 + PKCE login for the OrcaRouter provider
|
|
230
|
+
|
|
231
|
+
Usage:
|
|
232
|
+
lynkr connect orcarouter Interactive out-of-band code login
|
|
233
|
+
lynkr connect orcarouter --code <code> Non-interactive (paste the consent code)
|
|
234
|
+
lynkr connect orcarouter --api-key <key> Save an existing sk-orca-… API key
|
|
235
|
+
lynkr connect orcarouter --status Show masked credential status
|
|
236
|
+
lynkr connect orcarouter --clear Remove the stored credential`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
return runConnect(parseArgs(args));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (require.main === module || process.env._LYNKR_SUBCMD === "connect") {
|
|
243
|
+
main().catch((err) => fail(err.message));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = { runConnect, parseArgs, upsertEnvKey, clearEnvKey, readEnvKey, statusEnv, status };
|
package/bin/lynkr-init.js
CHANGED
|
@@ -188,12 +188,23 @@ const PROVIDERS = {
|
|
|
188
188
|
extras: [],
|
|
189
189
|
defaultModel: 'accounts/fireworks/models/kimi-k2-instruct-0905',
|
|
190
190
|
},
|
|
191
|
+
orcarouter: {
|
|
192
|
+
label: 'OrcaRouter (OpenAI-compatible gateway — adaptive routing, failover, zero-markup inference)',
|
|
193
|
+
local: false,
|
|
194
|
+
creds: [
|
|
195
|
+
{ key: 'ORCAROUTER_API_KEY', label: 'OrcaRouter API key (sk-orca-...; or run `lynkr connect orcarouter` for OAuth + PKCE login)', secret: true },
|
|
196
|
+
],
|
|
197
|
+
extras: [
|
|
198
|
+
{ key: 'ORCAROUTER_MODEL', label: 'default model', default: 'orcarouter/auto' },
|
|
199
|
+
],
|
|
200
|
+
defaultModel: 'orcarouter/auto',
|
|
201
|
+
},
|
|
191
202
|
};
|
|
192
203
|
|
|
193
204
|
const PROVIDER_ORDER = [
|
|
194
205
|
'ollama', 'llamacpp', 'lmstudio',
|
|
195
206
|
'azure-anthropic', 'azure-openai', 'openai', 'atlas', 'openrouter', 'edenai',
|
|
196
|
-
'databricks', 'bedrock', 'vertex', 'zai', 'moonshot', 'baidu', 'fireworks',
|
|
207
|
+
'databricks', 'bedrock', 'vertex', 'zai', 'moonshot', 'baidu', 'fireworks', 'orcarouter',
|
|
197
208
|
];
|
|
198
209
|
const TIERS = ['SIMPLE', 'MEDIUM', 'COMPLEX', 'REASONING'];
|
|
199
210
|
|
|
@@ -354,6 +365,9 @@ const BASELINE_ENV = {
|
|
|
354
365
|
FIREWORKS_API_KEY: '',
|
|
355
366
|
FIREWORKS_ENDPOINT: 'https://api.fireworks.ai/inference/v1/chat/completions',
|
|
356
367
|
FIREWORKS_MODEL: 'accounts/fireworks/models/kimi-k2-instruct-0905',
|
|
368
|
+
ORCAROUTER_API_KEY: '',
|
|
369
|
+
ORCAROUTER_MODEL: 'orcarouter/auto',
|
|
370
|
+
ORCAROUTER_ENDPOINT: 'https://api.orcarouter.ai/v1/chat/completions',
|
|
357
371
|
LLAMACPP_ENDPOINT: 'http://localhost:8080',
|
|
358
372
|
LLAMACPP_MODEL: 'default',
|
|
359
373
|
LLAMACPP_TIMEOUT_MS: '120000',
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"updatedAt": "2026-09-
|
|
4
|
-
"notes": "Reviewed per-family capability seeds. Values are curated estimates triangulated from SWE-Bench Verified, TerminalBench, LiveCodeBench/BigCodeBench and Artificial Analysis coding index (see scripts/seed-capabilities.js to regenerate from live sources). Heads: reasoning/codegen/debugging/tool_use in [0,1]. Keys are normalized family ids (see capability-seeds/family.js) with trailing-* wildcards matched longest-first. Quantized self-hosted servings take an automatic -0.02 haircut at resolve time. Unknown families fall through to the family heuristic, then tier-slot caps.",
|
|
3
|
+
"updatedAt": "2026-09-16",
|
|
4
|
+
"notes": "Reviewed per-family capability seeds. Values are curated estimates triangulated from SWE-Bench Verified, TerminalBench, LiveCodeBench/BigCodeBench and Artificial Analysis coding index (see scripts/seed-capabilities.js to regenerate from live sources). Heads: reasoning/codegen/debugging/tool_use in [0,1]. Keys are normalized family ids (see capability-seeds/family.js) with trailing-* wildcards matched longest-first. Quantized self-hosted servings take an automatic -0.02 haircut at resolve time. Unknown families fall through to the family heuristic, then tier-slot caps. 2026-09-16: added local-runtime + tier-catalog gaps (ornith, qwen2.5 base sizes, qwen3.5 sizes, spark-x2.5, gemma4, minimax-m3, versioned claude-3.5/gpt-4o/gpt-4.1 names, mistral bare, deepseek-coder, codellama, llama-3.1-8b, gemini-1.5-flash, amazon nova, gpt-4-turbo, gpt-3.5-turbo). Entries without benchmark rows are marked provisional — promote on data.",
|
|
5
5
|
"seeds": {
|
|
6
6
|
"claude-opus-4*": { "caps": { "reasoning": 0.9, "codegen": 0.9, "debugging": 0.9, "tool_use": 0.9 }, "sources": ["swe-bench-verified", "artificial-analysis"], "note": "curated-estimate: flagship tier" },
|
|
7
7
|
"claude-opus-3*": { "caps": { "reasoning": 0.8, "codegen": 0.82, "debugging": 0.8, "tool_use": 0.78 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
8
8
|
"claude-sonnet-4*": { "caps": { "reasoning": 0.78, "codegen": 0.8, "debugging": 0.78, "tool_use": 0.78 }, "sources": ["swe-bench-verified", "artificial-analysis"], "note": "curated-estimate" },
|
|
9
9
|
"claude-sonnet-3.5": { "caps": { "reasoning": 0.72, "codegen": 0.75, "debugging": 0.72, "tool_use": 0.7 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
10
|
+
"orcarouter-auto": { "caps": { "reasoning": 0.8, "codegen": 0.82, "debugging": 0.8, "tool_use": 0.82 }, "sources": ["provider-routing:adaptive auto"], "note": "curated-estimate: orcarouter/auto routes across frontier models per request" },
|
|
11
|
+
"claude-3.5-sonnet*": { "caps": { "reasoning": 0.72, "codegen": 0.75, "debugging": 0.72, "tool_use": 0.7 }, "sources": ["swe-bench-verified"], "note": "alias: dated/provider form (claude-3-5-sonnet-20241022) shares claude-sonnet-3.5 caps" },
|
|
10
12
|
"claude-haiku*": { "caps": { "reasoning": 0.42, "codegen": 0.48, "debugging": 0.42, "tool_use": 0.45 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
13
|
+
"claude-3.5-haiku*": { "caps": { "reasoning": 0.42, "codegen": 0.48, "debugging": 0.42, "tool_use": 0.45 }, "sources": ["swe-bench-verified"], "note": "alias: dated/provider form (claude-3-5-haiku-20241022) shares claude-haiku caps" },
|
|
11
14
|
"gpt-5.6*": { "caps": { "reasoning": 0.85, "codegen": 0.85, "debugging": 0.85, "tool_use": 0.82 }, "sources": ["swe-bench-verified"], "note": "curated-estimate: flagship tier" },
|
|
12
15
|
"gpt-5.4*": { "caps": { "reasoning": 0.85, "codegen": 0.85, "debugging": 0.85, "tool_use": 0.82 }, "sources": ["swe-bench-verified"], "note": "curated-estimate: flagship tier" },
|
|
13
16
|
"gpt-5.3*": { "caps": { "reasoning": 0.82, "codegen": 0.85, "debugging": 0.82, "tool_use": 0.8 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
@@ -15,10 +18,12 @@
|
|
|
15
18
|
"gpt-5.1*": { "caps": { "reasoning": 0.7, "codegen": 0.72, "debugging": 0.7, "tool_use": 0.68 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
16
19
|
"gpt-5-mini*": { "caps": { "reasoning": 0.6, "codegen": 0.65, "debugging": 0.6, "tool_use": 0.6 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
17
20
|
"gpt-5-nano*": { "caps": { "reasoning": 0.45, "codegen": 0.5, "debugging": 0.45, "tool_use": 0.45 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
18
|
-
"gpt-4o": { "caps": { "reasoning": 0.65, "codegen": 0.7, "debugging": 0.65, "tool_use": 0.68 }, "sources": ["swe-bench-verified", "livecodebench"], "note": "curated-estimate" },
|
|
21
|
+
"gpt-4o*": { "caps": { "reasoning": 0.65, "codegen": 0.7, "debugging": 0.65, "tool_use": 0.68 }, "sources": ["swe-bench-verified", "livecodebench"], "note": "curated-estimate; wildcard also covers dated snapshots (gpt-4o-2024-*). Exact gpt-4o-mini still wins via exact-first lookup." },
|
|
19
22
|
"gpt-4o-mini": { "caps": { "reasoning": 0.45, "codegen": 0.5, "debugging": 0.45, "tool_use": 0.48 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
20
|
-
"gpt-4.1": { "caps": { "reasoning": 0.68, "codegen": 0.72, "debugging": 0.68, "tool_use": 0.7 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
23
|
+
"gpt-4.1*": { "caps": { "reasoning": 0.68, "codegen": 0.72, "debugging": 0.68, "tool_use": 0.7 }, "sources": ["swe-bench-verified"], "note": "curated-estimate; wildcard also covers dated snapshots. Exact gpt-4.1-mini still wins via exact-first lookup." },
|
|
21
24
|
"gpt-4.1-mini": { "caps": { "reasoning": 0.5, "codegen": 0.55, "debugging": 0.5, "tool_use": 0.52 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
25
|
+
"gpt-4-turbo*": { "caps": { "reasoning": 0.62, "codegen": 0.68, "debugging": 0.62, "tool_use": 0.64 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: tier-catalog entry (MEDIUM openai slot); sized just below gpt-4o — promote on benchmark rows" },
|
|
26
|
+
"gpt-3.5-turbo": { "caps": { "reasoning": 0.3, "codegen": 0.35, "debugging": 0.3, "tool_use": 0.33 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: tier-catalog entry (SIMPLE openai slot); matches tiny/small ladder rung" },
|
|
22
27
|
"o3": { "caps": { "reasoning": 0.88, "codegen": 0.8, "debugging": 0.85, "tool_use": 0.7 }, "sources": ["swe-bench-verified"], "note": "curated-estimate: reasoning tilt" },
|
|
23
28
|
"o3-mini": { "caps": { "reasoning": 0.72, "codegen": 0.68, "debugging": 0.7, "tool_use": 0.6 }, "sources": ["swe-bench-verified"], "note": "curated-estimate: reasoning tilt" },
|
|
24
29
|
"o4-mini": { "caps": { "reasoning": 0.7, "codegen": 0.66, "debugging": 0.68, "tool_use": 0.6 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
@@ -28,11 +33,16 @@
|
|
|
28
33
|
"gemini-3-pro*": { "caps": { "reasoning": 0.85, "codegen": 0.83, "debugging": 0.85, "tool_use": 0.78 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
29
34
|
"gemini-2.5-flash": { "caps": { "reasoning": 0.55, "codegen": 0.6, "debugging": 0.55, "tool_use": 0.58 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
30
35
|
"gemini-2.0-flash": { "caps": { "reasoning": 0.45, "codegen": 0.5, "debugging": 0.45, "tool_use": 0.5 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
36
|
+
"gemini-1.5-flash*": { "caps": { "reasoning": 0.42, "codegen": 0.48, "debugging": 0.42, "tool_use": 0.46 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: tier-catalog entry (openrouter google/gemini-flash-1.5); sized just below gemini-2.0-flash" },
|
|
31
37
|
"gemini-1.5-pro": { "caps": { "reasoning": 0.6, "codegen": 0.62, "debugging": 0.6, "tool_use": 0.6 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
38
|
+
"amazon.nova-lite*": { "caps": { "reasoning": 0.35, "codegen": 0.4, "debugging": 0.35, "tool_use": 0.38 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: tier-catalog entry (bedrock SIMPLE slot); dotted key matches normalized amazon.nova-lite-v1-0 form" },
|
|
39
|
+
"amazon.nova-pro*": { "caps": { "reasoning": 0.5, "codegen": 0.55, "debugging": 0.5, "tool_use": 0.52 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: tier-catalog entry (bedrock MEDIUM slot)" },
|
|
32
40
|
"deepseek-r1": { "caps": { "reasoning": 0.82, "codegen": 0.78, "debugging": 0.8, "tool_use": 0.6 }, "sources": ["swe-bench-verified", "livecodebench"], "note": "curated-estimate: reasoning tilt, weak tool-use" },
|
|
33
41
|
"deepseek-reasoner": { "caps": { "reasoning": 0.82, "codegen": 0.78, "debugging": 0.8, "tool_use": 0.6 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
34
42
|
"deepseek-chat": { "caps": { "reasoning": 0.65, "codegen": 0.7, "debugging": 0.65, "tool_use": 0.62 }, "sources": ["livecodebench"], "note": "curated-estimate" },
|
|
35
43
|
"deepseek-v3*": { "caps": { "reasoning": 0.68, "codegen": 0.72, "debugging": 0.68, "tool_use": 0.62 }, "sources": ["swe-bench-verified", "livecodebench"], "note": "curated-estimate" },
|
|
44
|
+
"deepseek-coder*": { "caps": { "reasoning": 0.5, "codegen": 0.6, "debugging": 0.55, "tool_use": 0.5 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: tier-catalog legacy (MEDIUM/COMPLEX ollama slots: 33b, v2:236b); coder tilt, sized below qwen2.5-coder-32b" },
|
|
45
|
+
"codellama*": { "caps": { "reasoning": 0.45, "codegen": 0.55, "debugging": 0.5, "tool_use": 0.45 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: tier-catalog legacy (MEDIUM ollama slot: 34b); coder tilt" },
|
|
36
46
|
"qwen3-max": { "caps": { "reasoning": 0.78, "codegen": 0.8, "debugging": 0.78, "tool_use": 0.72 }, "sources": ["swe-bench-verified", "livecodebench"], "note": "curated-estimate" },
|
|
37
47
|
"qwen3-235b*": { "caps": { "reasoning": 0.78, "codegen": 0.8, "debugging": 0.78, "tool_use": 0.7 }, "sources": ["livecodebench"], "note": "curated-estimate" },
|
|
38
48
|
"qwen3-32b": { "caps": { "reasoning": 0.65, "codegen": 0.7, "debugging": 0.65, "tool_use": 0.6 }, "sources": ["livecodebench"], "note": "curated-estimate" },
|
|
@@ -40,8 +50,18 @@
|
|
|
40
50
|
"qwen2.5-coder-32b*": { "caps": { "reasoning": 0.55, "codegen": 0.68, "debugging": 0.6, "tool_use": 0.55 }, "sources": ["livecodebench"], "note": "curated-estimate: coder tilt" },
|
|
41
51
|
"qwen2.5-coder-14b*": { "caps": { "reasoning": 0.45, "codegen": 0.58, "debugging": 0.5, "tool_use": 0.45 }, "sources": ["livecodebench"], "note": "curated-estimate" },
|
|
42
52
|
"qwen2.5-coder-7b*": { "caps": { "reasoning": 0.35, "codegen": 0.48, "debugging": 0.4, "tool_use": 0.38 }, "sources": ["livecodebench"], "note": "curated-estimate" },
|
|
53
|
+
"qwen2.5-72b*": { "caps": { "reasoning": 0.55, "codegen": 0.62, "debugging": 0.55, "tool_use": 0.52 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: tier-catalog entry (COMPLEX ollama slot); sized at llama-3.1-70b rung" },
|
|
54
|
+
"qwen2.5-32b*": { "caps": { "reasoning": 0.5, "codegen": 0.6, "debugging": 0.55, "tool_use": 0.5 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: tier-catalog entry (MEDIUM ollama slot); sized just below qwen2.5-coder-32b" },
|
|
55
|
+
"qwen2.5-14b*": { "caps": { "reasoning": 0.42, "codegen": 0.55, "debugging": 0.48, "tool_use": 0.42 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: sized between qwen2.5-7b and qwen2.5-32b rungs" },
|
|
56
|
+
"qwen2.5-7b*": { "caps": { "reasoning": 0.33, "codegen": 0.45, "debugging": 0.38, "tool_use": 0.36 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: tier-catalog entry (SIMPLE ollama slot); sized just below qwen2.5-coder-7b" },
|
|
57
|
+
"qwen2.5-3b*": { "caps": { "reasoning": 0.28, "codegen": 0.35, "debugging": 0.3, "tool_use": 0.3 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: local runtime (ollama qwen2.5:3b); small-ladder rung" },
|
|
58
|
+
"qwen2.5-1.5b*": { "caps": { "reasoning": 0.22, "codegen": 0.28, "debugging": 0.24, "tool_use": 0.24 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: local runtime (ollama qwen2.5:1.5b); tiny-ladder rung" },
|
|
59
|
+
"qwen2.5-0.5b*": { "caps": { "reasoning": 0.15, "codegen": 0.2, "debugging": 0.18, "tool_use": 0.18 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: local runtime (ollama qwen2.5:0.5b); floor rung, trivial turns only" },
|
|
43
60
|
"qwen3-8b": { "caps": { "reasoning": 0.45, "codegen": 0.5, "debugging": 0.45, "tool_use": 0.45 }, "sources": ["livecodebench"], "note": "curated-estimate" },
|
|
44
61
|
"qwen3-4b": { "caps": { "reasoning": 0.35, "codegen": 0.42, "debugging": 0.38, "tool_use": 0.38 }, "sources": ["livecodebench"], "note": "curated-estimate" },
|
|
62
|
+
"qwen3.5-9b*": { "caps": { "reasoning": 0.42, "codegen": 0.5, "debugging": 0.44, "tool_use": 0.42 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: Qwen3.5 9B class (ornith arch family); sized just below qwen3-8b+ — promote on benchmark rows" },
|
|
63
|
+
"qwen3.5-4b*": { "caps": { "reasoning": 0.35, "codegen": 0.42, "debugging": 0.38, "tool_use": 0.38 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: Qwen3.5 4B class; mirrors qwen3-4b — promote on benchmark rows" },
|
|
64
|
+
"qwen3.5-2b*": { "caps": { "reasoning": 0.26, "codegen": 0.3, "debugging": 0.27, "tool_use": 0.28 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: Qwen3.5 2B class; tiny/small rung" },
|
|
45
65
|
"glm-5.2": { "caps": { "reasoning": 0.68, "codegen": 0.72, "debugging": 0.68, "tool_use": 0.66 }, "sources": ["swe-bench-verified", "artificial-analysis"], "note": "curated-estimate: provider-agnostic (z.ai/Baidu/local share this entry)" },
|
|
46
66
|
"glm-5*": { "caps": { "reasoning": 0.65, "codegen": 0.68, "debugging": 0.65, "tool_use": 0.62 }, "sources": ["artificial-analysis"], "note": "curated-estimate: GLM-5 family default" },
|
|
47
67
|
"glm-4.7": { "caps": { "reasoning": 0.62, "codegen": 0.66, "debugging": 0.62, "tool_use": 0.6 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
@@ -54,20 +74,28 @@
|
|
|
54
74
|
"kimi-k2*": { "caps": { "reasoning": 0.6, "codegen": 0.63, "debugging": 0.6, "tool_use": 0.6 }, "sources": ["swe-bench-verified"], "note": "curated-estimate: Kimi-K2 family default" },
|
|
55
75
|
"llama-3.3-70b*": { "caps": { "reasoning": 0.6, "codegen": 0.65, "debugging": 0.6, "tool_use": 0.58 }, "sources": ["swe-bench-verified", "livecodebench"], "note": "curated-estimate" },
|
|
56
76
|
"llama-3.1-70b*": { "caps": { "reasoning": 0.55, "codegen": 0.6, "debugging": 0.55, "tool_use": 0.52 }, "sources": ["livecodebench"], "note": "curated-estimate" },
|
|
77
|
+
"llama-3.1-8b*": { "caps": { "reasoning": 0.35, "codegen": 0.4, "debugging": 0.35, "tool_use": 0.36 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: tier-catalog entry (fireworks llama-3.1-8b-instruct); small rung" },
|
|
57
78
|
"llama-4-maverick*": { "caps": { "reasoning": 0.6, "codegen": 0.62, "debugging": 0.6, "tool_use": 0.58 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
58
79
|
"llama-4-scout*": { "caps": { "reasoning": 0.5, "codegen": 0.52, "debugging": 0.5, "tool_use": 0.5 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
59
80
|
"llama-3.2*": { "caps": { "reasoning": 0.25, "codegen": 0.3, "debugging": 0.25, "tool_use": 0.28 }, "sources": ["livecodebench"], "note": "curated-estimate" },
|
|
60
81
|
"mistral-large*": { "caps": { "reasoning": 0.6, "codegen": 0.65, "debugging": 0.6, "tool_use": 0.62 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
61
82
|
"codestral*": { "caps": { "reasoning": 0.5, "codegen": 0.62, "debugging": 0.55, "tool_use": 0.5 }, "sources": ["livecodebench"], "note": "curated-estimate: coder tilt" },
|
|
62
83
|
"mistral-small*": { "caps": { "reasoning": 0.45, "codegen": 0.5, "debugging": 0.45, "tool_use": 0.48 }, "sources": ["artificial-analysis"], "note": "curated-estimate" },
|
|
84
|
+
"mistral*": { "caps": { "reasoning": 0.35, "codegen": 0.4, "debugging": 0.35, "tool_use": 0.36 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: bare/7b-class mistral (ollama SIMPLE slot); longer keys (mistral-large/small) still win via longest-prefix" },
|
|
63
85
|
"devstral*": { "caps": { "reasoning": 0.5, "codegen": 0.6, "debugging": 0.55, "tool_use": 0.5 }, "sources": ["swe-bench-verified"], "note": "curated-estimate: coder tilt" },
|
|
64
86
|
"gpt-oss-120b": { "caps": { "reasoning": 0.65, "codegen": 0.68, "debugging": 0.65, "tool_use": 0.55 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
65
87
|
"gpt-oss-20b": { "caps": { "reasoning": 0.45, "codegen": 0.5, "debugging": 0.45, "tool_use": 0.42 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
66
88
|
"gemma-3-27b": { "caps": { "reasoning": 0.45, "codegen": 0.48, "debugging": 0.45, "tool_use": 0.45 }, "sources": ["livecodebench"], "note": "curated-estimate" },
|
|
89
|
+
"gemma4*": { "caps": { "reasoning": 0.42, "codegen": 0.46, "debugging": 0.42, "tool_use": 0.42 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: local runtime (ollama gemma4:12b normalizes to gemma4-12b, no dash); sized just below gemma-3-27b" },
|
|
67
90
|
"gemma-2*": { "caps": { "reasoning": 0.28, "codegen": 0.32, "debugging": 0.28, "tool_use": 0.3 }, "sources": ["livecodebench"], "note": "curated-estimate" },
|
|
68
91
|
"phi-4": { "caps": { "reasoning": 0.45, "codegen": 0.48, "debugging": 0.45, "tool_use": 0.4 }, "sources": ["livecodebench"], "note": "curated-estimate" },
|
|
69
92
|
"phi-3*": { "caps": { "reasoning": 0.28, "codegen": 0.32, "debugging": 0.28, "tool_use": 0.28 }, "sources": ["livecodebench"], "note": "curated-estimate" },
|
|
70
93
|
"minimax-m2*": { "caps": { "reasoning": 0.65, "codegen": 0.68, "debugging": 0.65, "tool_use": 0.65 }, "sources": ["swe-bench-verified"], "note": "curated-estimate" },
|
|
94
|
+
"minimax-m3*": { "caps": { "reasoning": 0.65, "codegen": 0.68, "debugging": 0.65, "tool_use": 0.65 }, "sources": ["lynkr-operator-estimate"], "note": "provisional: local runtime (OLLAMA_MODEL minimax-m3:cloud); mirrors minimax-m2 pending m3 rows" },
|
|
95
|
+
"ornith*": { "caps": { "reasoning": 0.44, "codegen": 0.52, "debugging": 0.46, "tool_use": 0.47 }, "sources": ["ollama-library", "lynkr-operator-estimate"], "note": "provisional: 9B Qwen3.5-based agentic coder (tools+thinking, 262k ctx); no public SWE-bench row — coder/agentic tilt, revisit on data" },
|
|
96
|
+
"spark-x2.5-4b*": { "caps": { "reasoning": 0.42, "codegen": 0.5, "debugging": 0.45, "tool_use": 0.48 }, "sources": ["vendor-reported", "lynkr-operator-estimate"], "note": "provisional: vendor claims SOTA-among-size on agent/code; capped per self-report policy (cf. muse-spark-1.3) — revisit on independent rows" },
|
|
97
|
+
"spark-x2.5-1.7b*": { "caps": { "reasoning": 0.32, "codegen": 0.38, "debugging": 0.34, "tool_use": 0.36 }, "sources": ["vendor-reported", "lynkr-operator-estimate"], "note": "provisional: 1.7B sibling; small rung with agent tilt — revisit on independent rows" },
|
|
98
|
+
"spark-x2.5*": { "caps": { "reasoning": 0.35, "codegen": 0.42, "debugging": 0.38, "tool_use": 0.4 }, "sources": ["lynkr-operator-estimate"], "note": "family default; dated/suffixed forms fall here, longer 4b/1.7b keys win via longest-prefix" },
|
|
71
99
|
"muse-spark-1.3": { "caps": { "reasoning": 0.85, "codegen": 0.87, "debugging": 0.85, "tool_use": 0.75 }, "sources": ["deepswe-1.1@75.4% (vendor-reported, max setting)", "terminal-bench-2.1@88.8% (vendor-reported)"], "note": "curated-estimate: full 1.3 at max reasoning. Vendor self-reported; absent from public DeepSWE leaderboard (only 1.2 listed); hands-on gap reported by third parties. tool_use capped — no direct tool-use data. Revisit on independent rows." },
|
|
72
100
|
"muse-spark-1.2*": { "caps": { "reasoning": 0.42, "codegen": 0.46, "debugging": 0.42, "tool_use": 0.44 }, "sources": ["lynkr-telemetry:4176 scored rows, avg quality 52.7 @ complexity 51.7"], "note": "measured: operator's own traffic. Below the 70 evidence bar — economize trivial turns only." },
|
|
73
101
|
"muse-spark-1.3-contributor*": { "caps": { "reasoning": 0.4, "codegen": 0.45, "debugging": 0.4, "tool_use": 0.45 }, "sources": ["lynkr-telemetry:29 scored rows (thin) + 1.2 family history"], "note": "provisional: free/contributor serving grades far below full-1.3 published scores (variant, effort setting, or benchmark inflation — undetermined). Do NOT cover with muse-spark*: the full model and the free serving must never share caps." }
|
package/package.json
CHANGED
|
@@ -8,12 +8,21 @@ const FALLBACK_HEADERS = [
|
|
|
8
8
|
"x-claude-session",
|
|
9
9
|
"x-claude-conversation-id",
|
|
10
10
|
"anthropic-session-id",
|
|
11
|
+
// open-code-review per-bundle affinity: internal/llm/sessionkey.go sends
|
|
12
|
+
// `x-session-affinity: <ocr_session_key>` (real task key, or an
|
|
13
|
+
// auto-generated fallback). Each file bundle gets its own key so bundles
|
|
14
|
+
// route independently instead of sharing one PR-wide pin.
|
|
15
|
+
"x-session-affinity",
|
|
11
16
|
];
|
|
12
17
|
|
|
13
18
|
function normaliseSessionId(value) {
|
|
14
19
|
if (typeof value !== "string") return null;
|
|
15
20
|
const trimmed = value.trim();
|
|
16
|
-
|
|
21
|
+
if (!trimmed.length) return null;
|
|
22
|
+
// Ignore unsubstituted templates (OCR sends literal "{ocr_session_key}"
|
|
23
|
+
// when no key is configured — its own client falls back to auto-generated).
|
|
24
|
+
if (trimmed.includes("{") && trimmed.includes("}")) return null;
|
|
25
|
+
return trimmed;
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
function _contentText(content) {
|
package/src/api/openai-router.js
CHANGED
|
@@ -986,9 +986,10 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
986
986
|
|
|
987
987
|
/**
|
|
988
988
|
* Get all configured providers with their models (cc-relay style)
|
|
989
|
-
* Reads from config (which comes from .env) to discover what's available
|
|
989
|
+
* Reads from config (which comes from .env) to discover what's available.
|
|
990
|
+
* OrcaRouter contributes its live capability-filtered chat catalog.
|
|
990
991
|
*/
|
|
991
|
-
function getConfiguredProviders() {
|
|
992
|
+
async function getConfiguredProviders() {
|
|
992
993
|
const providers = [];
|
|
993
994
|
|
|
994
995
|
if (config.databricks?.url && config.databricks?.apiKey) {
|
|
@@ -1156,6 +1157,23 @@ function getConfiguredProviders() {
|
|
|
1156
1157
|
});
|
|
1157
1158
|
}
|
|
1158
1159
|
|
|
1160
|
+
// OrcaRouter (OpenAI-compatible gateway). The model list is the live
|
|
1161
|
+
// capability-filtered catalog fetch (authoritative when the account key
|
|
1162
|
+
// works), with a verified seed fallback. Never a hand-written example list.
|
|
1163
|
+
if (config.orcarouter?.apiKey) {
|
|
1164
|
+
try {
|
|
1165
|
+
const { getOrcaChatModels } = require("../clients/orcarouter-catalog");
|
|
1166
|
+
const { models } = await getOrcaChatModels(config.orcarouter, { capability: "chat" });
|
|
1167
|
+
providers.push({
|
|
1168
|
+
name: "orcarouter",
|
|
1169
|
+
type: "orcarouter",
|
|
1170
|
+
models: models.map(m => m.id),
|
|
1171
|
+
});
|
|
1172
|
+
} catch (err) {
|
|
1173
|
+
logger.debug({ err: err.message }, "OrcaRouter discovery failed — omitting from OpenAI /v1/models");
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1159
1177
|
if (config.vertex?.projectId) {
|
|
1160
1178
|
providers.push({
|
|
1161
1179
|
name: "vertex",
|
|
@@ -1178,9 +1196,9 @@ function getConfiguredProviders() {
|
|
|
1178
1196
|
* List available models from ALL configured providers (cc-relay style).
|
|
1179
1197
|
* Returns OpenAI-compatible model list with provider field.
|
|
1180
1198
|
*/
|
|
1181
|
-
router.get("/models", (req, res) => {
|
|
1199
|
+
router.get("/models", async (req, res) => {
|
|
1182
1200
|
try {
|
|
1183
|
-
const providers = getConfiguredProviders();
|
|
1201
|
+
const providers = await getConfiguredProviders();
|
|
1184
1202
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
1185
1203
|
const models = [];
|
|
1186
1204
|
const seenModelIds = new Set();
|
|
@@ -17,9 +17,11 @@ const router = express.Router();
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Get all configured providers with their models
|
|
20
|
-
* Reads from config (which comes from .env) to discover what's available
|
|
20
|
+
* Reads from config (which comes from .env) to discover what's available.
|
|
21
|
+
* OrcaRouter's model list is a live capability-filtered catalog fetch
|
|
22
|
+
* (authoritative when the account key works), with a verified seed fallback.
|
|
21
23
|
*/
|
|
22
|
-
function getConfiguredProviders() {
|
|
24
|
+
async function getConfiguredProviders() {
|
|
23
25
|
const providers = [];
|
|
24
26
|
|
|
25
27
|
// Check Databricks
|
|
@@ -253,6 +255,27 @@ function getConfiguredProviders() {
|
|
|
253
255
|
});
|
|
254
256
|
}
|
|
255
257
|
|
|
258
|
+
// Check OrcaRouter (OpenAI-compatible gateway). The model list is the live
|
|
259
|
+
// capability-filtered catalog (text chat) — authoritative when discovery
|
|
260
|
+
// succeeds, verified seed otherwise. Never a hand-written example list.
|
|
261
|
+
if (config.orcarouter?.apiKey) {
|
|
262
|
+
try {
|
|
263
|
+
const { getOrcaChatModels } = require("../clients/orcarouter-catalog");
|
|
264
|
+
const { models, source, degraded } = await getOrcaChatModels(config.orcarouter, { capability: "chat" });
|
|
265
|
+
providers.push({
|
|
266
|
+
name: "orcarouter",
|
|
267
|
+
type: "orcarouter",
|
|
268
|
+
baseUrl: `${config.orcarouter.apiBaseUrl || "https://api.orcarouter.ai"}/v1`,
|
|
269
|
+
enabled: true,
|
|
270
|
+
catalog_source: source,
|
|
271
|
+
catalog_degraded: degraded,
|
|
272
|
+
models: models.map(m => ({ id: m.id, name: m.name || m.id })),
|
|
273
|
+
});
|
|
274
|
+
} catch (err) {
|
|
275
|
+
logger.debug({ err: err.message }, "OrcaRouter provider discovery failed — omitting from /v1/models");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
256
279
|
// Check Vertex AI (Google Cloud)
|
|
257
280
|
if (config.vertex?.projectId) {
|
|
258
281
|
const region = config.vertex.region || "us-east5";
|
|
@@ -287,9 +310,9 @@ function getPrimaryProvider() {
|
|
|
287
310
|
* Anthropic-compatible model listing endpoint (cc-relay style).
|
|
288
311
|
* Lists all available models from all configured providers.
|
|
289
312
|
*/
|
|
290
|
-
router.get("/models", (req, res) => {
|
|
313
|
+
router.get("/models", async (req, res) => {
|
|
291
314
|
try {
|
|
292
|
-
const providers = getConfiguredProviders();
|
|
315
|
+
const providers = await getConfiguredProviders();
|
|
293
316
|
const primaryProvider = getPrimaryProvider();
|
|
294
317
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
295
318
|
|
|
@@ -344,9 +367,9 @@ router.get("/models", (req, res) => {
|
|
|
344
367
|
* Provider listing endpoint (cc-relay style).
|
|
345
368
|
* Lists all configured providers with their metadata and models.
|
|
346
369
|
*/
|
|
347
|
-
router.get("/providers", (req, res) => {
|
|
370
|
+
router.get("/providers", async (req, res) => {
|
|
348
371
|
try {
|
|
349
|
-
const providers = getConfiguredProviders();
|
|
372
|
+
const providers = await getConfiguredProviders();
|
|
350
373
|
const primaryProvider = getPrimaryProvider();
|
|
351
374
|
const fallbackProvider = config.modelProvider?.fallbackProvider;
|
|
352
375
|
|
|
@@ -391,10 +414,10 @@ router.get("/providers", (req, res) => {
|
|
|
391
414
|
*
|
|
392
415
|
* Get details for a specific provider.
|
|
393
416
|
*/
|
|
394
|
-
router.get("/providers/:name", (req, res) => {
|
|
417
|
+
router.get("/providers/:name", async (req, res) => {
|
|
395
418
|
try {
|
|
396
419
|
const providerName = req.params.name.toLowerCase();
|
|
397
|
-
const providers = getConfiguredProviders();
|
|
420
|
+
const providers = await getConfiguredProviders();
|
|
398
421
|
const provider = providers.find(p => p.name === providerName);
|
|
399
422
|
|
|
400
423
|
if (!provider) {
|
|
@@ -435,9 +458,9 @@ router.get("/providers/:name", (req, res) => {
|
|
|
435
458
|
*
|
|
436
459
|
* Get current configuration summary (without sensitive data).
|
|
437
460
|
*/
|
|
438
|
-
router.get("/config", (req, res) => {
|
|
461
|
+
router.get("/config", async (req, res) => {
|
|
439
462
|
try {
|
|
440
|
-
const providers = getConfiguredProviders();
|
|
463
|
+
const providers = await getConfiguredProviders();
|
|
441
464
|
|
|
442
465
|
res.json({
|
|
443
466
|
model_provider: config.modelProvider?.type || "databricks",
|