tau-coding-agent 0.1.5 → 0.1.6
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/extensions/answer.ts +11 -5
- package/extensions/btw.ts +73 -2
- package/extensions/insights.ts +38 -1
- package/extensions/loop.ts +14 -3
- package/extensions/memory.ts +37 -14
- package/extensions/review/models.ts +1 -1
- package/extensions/review/prompts.ts +4 -4
- package/extensions/review/submit-review-tool.ts +1 -5
- package/extensions/review/submit-triage-tool.ts +1 -0
- package/extensions/sandbox/index.ts +595 -101
- package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
- package/extensions/usage/anthropic.ts +5 -6
- package/extensions/usage/github-copilot.ts +30 -9
- package/extensions/usage/index.ts +1 -1
- package/extensions/usage/openai-codex.ts +2 -2
- package/extensions/websearch/providers/pi-model.shared.ts +19 -1
- package/package.json +2 -2
- package/skills/oracle/SKILL.md +3 -3
- package/skills/oracle/scripts/oracle +54 -50
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compatibility workaround for sandbox-runtime on macOS.
|
|
5
|
+
*
|
|
6
|
+
* The runtime routes Git-over-SSH through BSD nc, which cannot authenticate to
|
|
7
|
+
* its SOCKS5 proxy. This shell preserves normal bash execution while replacing
|
|
8
|
+
* that transport with an authenticated HTTP CONNECT tunnel.
|
|
9
|
+
*
|
|
10
|
+
* Remove once the upstream fix ships:
|
|
11
|
+
* https://github.com/anthropic-experimental/sandbox-runtime/pull/385
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { connect } from "node:net";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const PROXY_CONNECT_MODE = "--proxy-connect";
|
|
19
|
+
const CONNECT_TIMEOUT_MS = 15_000;
|
|
20
|
+
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
|
21
|
+
|
|
22
|
+
if (process.argv[2] === PROXY_CONNECT_MODE) {
|
|
23
|
+
connectThroughSandboxProxy(process.argv[3], process.argv[4]);
|
|
24
|
+
} else {
|
|
25
|
+
runBash(process.argv.slice(2));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function runBash(args) {
|
|
29
|
+
if (hasAuthenticatedSandboxProxy()) {
|
|
30
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
31
|
+
const proxyCommand = [
|
|
32
|
+
shellQuote(escapeSshPercent(process.execPath)),
|
|
33
|
+
shellQuote(escapeSshPercent(scriptPath)),
|
|
34
|
+
PROXY_CONNECT_MODE,
|
|
35
|
+
"%h",
|
|
36
|
+
"%p",
|
|
37
|
+
].join(" ");
|
|
38
|
+
process.env.GIT_SSH_COMMAND = [
|
|
39
|
+
"ssh",
|
|
40
|
+
"-o ControlMaster=no",
|
|
41
|
+
"-o ControlPath=none",
|
|
42
|
+
`-o ProxyCommand=${shellQuote(proxyCommand)}`,
|
|
43
|
+
].join(" ");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const child = spawn("bash", args, {
|
|
47
|
+
env: process.env,
|
|
48
|
+
stdio: "inherit",
|
|
49
|
+
});
|
|
50
|
+
child.once("error", (error) => {
|
|
51
|
+
console.error(error.message);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
});
|
|
54
|
+
child.once("exit", (code, signal) => {
|
|
55
|
+
if (signal) {
|
|
56
|
+
process.kill(process.pid, signal);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
process.exitCode = code ?? 1;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasAuthenticatedSandboxProxy() {
|
|
64
|
+
try {
|
|
65
|
+
const proxy = getSandboxProxyUrl();
|
|
66
|
+
return proxy.protocol === "http:" && Boolean(proxy.username || proxy.password);
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function connectThroughSandboxProxy(targetHost, targetPortText) {
|
|
73
|
+
const targetPort = Number(targetPortText);
|
|
74
|
+
if (
|
|
75
|
+
!targetHost ||
|
|
76
|
+
/[\r\n]/u.test(targetHost) ||
|
|
77
|
+
!Number.isInteger(targetPort) ||
|
|
78
|
+
targetPort < 1 ||
|
|
79
|
+
targetPort > 65_535
|
|
80
|
+
) {
|
|
81
|
+
fail("Invalid SSH proxy target.");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let proxy;
|
|
85
|
+
try {
|
|
86
|
+
proxy = getSandboxProxyUrl();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
fail(error);
|
|
89
|
+
}
|
|
90
|
+
if (proxy.protocol !== "http:") {
|
|
91
|
+
fail(`Unsupported sandbox proxy protocol: ${proxy.protocol}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const proxyPort = Number(proxy.port || 80);
|
|
95
|
+
const authority = targetHost.includes(":")
|
|
96
|
+
? `[${targetHost}]:${targetPort}`
|
|
97
|
+
: `${targetHost}:${targetPort}`;
|
|
98
|
+
let username;
|
|
99
|
+
let password;
|
|
100
|
+
try {
|
|
101
|
+
username = decodeURIComponent(proxy.username);
|
|
102
|
+
password = decodeURIComponent(proxy.password);
|
|
103
|
+
} catch {
|
|
104
|
+
fail("Sandbox HTTP proxy credentials are invalid.");
|
|
105
|
+
}
|
|
106
|
+
const authorization =
|
|
107
|
+
username || password
|
|
108
|
+
? `Proxy-Authorization: Basic ${Buffer.from(`${username}:${password}`).toString("base64")}\r\n`
|
|
109
|
+
: "";
|
|
110
|
+
|
|
111
|
+
const socket = connect({ host: proxy.hostname, port: proxyPort });
|
|
112
|
+
const connectTimeout = setTimeout(() => {
|
|
113
|
+
socket.destroy(new Error("Sandbox proxy connection timed out."));
|
|
114
|
+
}, CONNECT_TIMEOUT_MS);
|
|
115
|
+
let response = Buffer.alloc(0);
|
|
116
|
+
let tunnelEstablished = false;
|
|
117
|
+
let connectionFailed = false;
|
|
118
|
+
|
|
119
|
+
socket.once("connect", () => {
|
|
120
|
+
clearTimeout(connectTimeout);
|
|
121
|
+
socket.write(
|
|
122
|
+
`CONNECT ${authority} HTTP/1.1\r\nHost: ${authority}\r\n${authorization}Proxy-Connection: Keep-Alive\r\n\r\n`,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const failConnection = (error) => {
|
|
127
|
+
if (connectionFailed) return;
|
|
128
|
+
connectionFailed = true;
|
|
129
|
+
clearTimeout(connectTimeout);
|
|
130
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
socket.destroy();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const handleResponse = (chunk) => {
|
|
136
|
+
response = Buffer.concat([response, chunk]);
|
|
137
|
+
if (response.length > MAX_RESPONSE_HEADER_BYTES) {
|
|
138
|
+
failConnection(new Error("Sandbox proxy returned oversized headers."));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const headerEnd = response.indexOf("\r\n\r\n");
|
|
143
|
+
if (headerEnd === -1) return;
|
|
144
|
+
|
|
145
|
+
const header = response.subarray(0, headerEnd).toString("latin1");
|
|
146
|
+
const status = header.match(/^HTTP\/\d(?:\.\d)?\s+(\d{3})/u)?.[1];
|
|
147
|
+
if (status !== "200") {
|
|
148
|
+
failConnection(new Error(`Sandbox proxy CONNECT failed${status ? ` (${status})` : ""}.`));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
tunnelEstablished = true;
|
|
153
|
+
socket.off("data", handleResponse);
|
|
154
|
+
const remainder = response.subarray(headerEnd + 4);
|
|
155
|
+
if (remainder.length > 0) process.stdout.write(remainder);
|
|
156
|
+
process.stdin.pipe(socket);
|
|
157
|
+
socket.pipe(process.stdout);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
socket.on("data", handleResponse);
|
|
161
|
+
socket.once("error", failConnection);
|
|
162
|
+
socket.once("close", () => {
|
|
163
|
+
clearTimeout(connectTimeout);
|
|
164
|
+
process.stdin.unpipe(socket);
|
|
165
|
+
process.stdin.pause();
|
|
166
|
+
if (!tunnelEstablished && !connectionFailed) {
|
|
167
|
+
failConnection(new Error("Sandbox proxy closed before establishing the SSH tunnel."));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
process.stdin.on("error", failConnection);
|
|
171
|
+
process.stdout.on("error", (error) => {
|
|
172
|
+
if (error?.code === "EPIPE") {
|
|
173
|
+
socket.destroy();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
failConnection(error);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getSandboxProxyUrl() {
|
|
181
|
+
const value = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY;
|
|
182
|
+
if (!value) throw new Error("Sandbox HTTP proxy is unavailable.");
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
return new URL(value);
|
|
186
|
+
} catch {
|
|
187
|
+
throw new Error("Sandbox HTTP proxy URL is invalid.");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function escapeSshPercent(value) {
|
|
192
|
+
return value.replaceAll("%", "%%");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function shellQuote(value) {
|
|
196
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function fail(error) {
|
|
200
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { readStoredCredential, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
clampPercent,
|
|
4
4
|
formatCurrency,
|
|
@@ -111,7 +111,7 @@ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<
|
|
|
111
111
|
const token = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
|
|
112
112
|
if (!token) throw new LiveUsageUnavailableError("no auth configured");
|
|
113
113
|
|
|
114
|
-
const credential =
|
|
114
|
+
const credential = readStoredCredential(PROVIDER_ID);
|
|
115
115
|
if (credential?.type === "api_key") {
|
|
116
116
|
throw new LiveUsageUnavailableError("API-key auth does not expose Claude subscription quota");
|
|
117
117
|
}
|
|
@@ -170,12 +170,11 @@ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<
|
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
async function getLiveUsageAvailability(
|
|
174
|
-
|
|
173
|
+
async function getLiveUsageAvailability(_ctx: ExtensionContext) {
|
|
174
|
+
const credential = readStoredCredential(PROVIDER_ID);
|
|
175
|
+
if (!credential) {
|
|
175
176
|
return { available: false as const, reason: "no auth configured" };
|
|
176
177
|
}
|
|
177
|
-
|
|
178
|
-
const credential = ctx.modelRegistry.authStorage.get(PROVIDER_ID);
|
|
179
178
|
if (credential?.type === "oauth") {
|
|
180
179
|
return { available: true as const };
|
|
181
180
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { OAuthCredential } from "@earendil-works/pi-ai";
|
|
2
|
+
import { readStoredCredential, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import {
|
|
3
4
|
clampPercent,
|
|
4
5
|
formatCount,
|
|
@@ -8,7 +9,12 @@ import {
|
|
|
8
9
|
readNumber,
|
|
9
10
|
readString,
|
|
10
11
|
} from "./shared.js";
|
|
11
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
LiveUsageAvailability,
|
|
14
|
+
LiveUsageItem,
|
|
15
|
+
LiveUsageSnapshot,
|
|
16
|
+
UsageProviderDefinition,
|
|
17
|
+
} from "./types.js";
|
|
12
18
|
|
|
13
19
|
const PROVIDER_ID = "github-copilot";
|
|
14
20
|
|
|
@@ -136,13 +142,18 @@ function buildUsageUrl(enterpriseUrl?: string): string {
|
|
|
136
142
|
return `https://${host}/copilot_internal/user`;
|
|
137
143
|
}
|
|
138
144
|
|
|
139
|
-
async function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
async function getLiveUsageAvailability(_ctx: ExtensionContext): Promise<LiveUsageAvailability> {
|
|
146
|
+
return getStoredCopilotCredential()
|
|
147
|
+
? { available: true }
|
|
148
|
+
: { available: false, reason: "not logged in" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function fetchUsage(
|
|
152
|
+
_ctx: ExtensionContext,
|
|
153
|
+
signal?: AbortSignal,
|
|
154
|
+
): Promise<LiveUsageSnapshot> {
|
|
155
|
+
const credential = getStoredCopilotCredential();
|
|
156
|
+
if (!credential) {
|
|
146
157
|
throw new Error("Not logged in to GitHub Copilot.");
|
|
147
158
|
}
|
|
148
159
|
|
|
@@ -193,11 +204,21 @@ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<
|
|
|
193
204
|
};
|
|
194
205
|
}
|
|
195
206
|
|
|
207
|
+
function getStoredCopilotCredential(): OAuthCredential | undefined {
|
|
208
|
+
const credential = readStoredCredential(PROVIDER_ID);
|
|
209
|
+
return credential?.type === "oauth" &&
|
|
210
|
+
typeof credential.refresh === "string" &&
|
|
211
|
+
credential.refresh.trim()
|
|
212
|
+
? credential
|
|
213
|
+
: undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
196
216
|
const githubCopilotProvider: UsageProviderDefinition = {
|
|
197
217
|
id: PROVIDER_ID,
|
|
198
218
|
label: "GitHub Copilot",
|
|
199
219
|
shortLabel: "copilot",
|
|
200
220
|
color: { r: 168, g: 85, b: 247 },
|
|
221
|
+
getLiveUsageAvailability,
|
|
201
222
|
fetchLiveUsage: fetchUsage,
|
|
202
223
|
};
|
|
203
224
|
|
|
@@ -1433,7 +1433,7 @@ async function getLiveUsageAvailability(
|
|
|
1433
1433
|
return provider.getLiveUsageAvailability(ctx);
|
|
1434
1434
|
}
|
|
1435
1435
|
|
|
1436
|
-
return ctx.modelRegistry.
|
|
1436
|
+
return ctx.modelRegistry.getProviderAuthStatus(provider.id).configured
|
|
1437
1437
|
? { available: true }
|
|
1438
1438
|
: { available: false, reason: "no auth configured" };
|
|
1439
1439
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { readStoredCredential, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
clampPercent,
|
|
4
4
|
formatCurrency,
|
|
@@ -112,7 +112,7 @@ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<
|
|
|
112
112
|
const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
|
|
113
113
|
if (!apiKey) throw new Error("Not logged in to OpenAI Codex.");
|
|
114
114
|
|
|
115
|
-
const credential =
|
|
115
|
+
const credential = readStoredCredential(PROVIDER_ID);
|
|
116
116
|
const accountId =
|
|
117
117
|
credential?.type === "oauth" && typeof credential.accountId === "string"
|
|
118
118
|
? credential.accountId
|
|
@@ -5,6 +5,7 @@ export interface PiModelSelection {
|
|
|
5
5
|
model: Model<Api>;
|
|
6
6
|
apiKey?: string;
|
|
7
7
|
headers?: Record<string, string>;
|
|
8
|
+
env?: Record<string, string>;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export async function selectCurrentPiModel(
|
|
@@ -42,7 +43,24 @@ async function resolvePiModelSelection(
|
|
|
42
43
|
ctx: Pick<ExtensionContext, "modelRegistry">,
|
|
43
44
|
): Promise<PiModelSelection | null> {
|
|
44
45
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
45
|
-
return auth.ok
|
|
46
|
+
return auth.ok
|
|
47
|
+
? {
|
|
48
|
+
model: resolveModelEnv(model, auth.env),
|
|
49
|
+
apiKey: auth.apiKey,
|
|
50
|
+
headers: auth.headers,
|
|
51
|
+
env: auth.env,
|
|
52
|
+
}
|
|
53
|
+
: null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveModelEnv(model: Model<Api>, env?: Record<string, string>): Model<Api> {
|
|
57
|
+
if (!model.baseUrl || !env) return model;
|
|
58
|
+
|
|
59
|
+
let baseUrl = model.baseUrl;
|
|
60
|
+
for (const [name, value] of Object.entries(env)) {
|
|
61
|
+
baseUrl = baseUrl.replaceAll(`{${name}}`, value);
|
|
62
|
+
}
|
|
63
|
+
return baseUrl === model.baseUrl ? model : { ...model, baseUrl };
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
function rankPiModels(models: Model<Api>[]): Model<Api>[] {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tau-coding-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Opinionated Pi package with web search, review workflows, sandboxing, memory, usage reporting, skills, and a theme",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-tools",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"type": "module",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@anthropic-ai/sandbox-runtime": "^0.0.
|
|
30
|
+
"@anthropic-ai/sandbox-runtime": "^0.0.67",
|
|
31
31
|
"@mozilla/readability": "^0.6.0",
|
|
32
32
|
"jsdom": "^29.0.2",
|
|
33
33
|
"puppeteer-core": "^24.40.0",
|
package/skills/oracle/SKILL.md
CHANGED
|
@@ -43,14 +43,14 @@ Run commands from this skill directory:
|
|
|
43
43
|
./scripts/oracle-bundle -p "<task>" --file "src/**" --file "!**/*.test.*"
|
|
44
44
|
|
|
45
45
|
# Show which oracle model would be selected
|
|
46
|
-
./scripts/oracle --list-models --current openai-codex/gpt-5.
|
|
46
|
+
./scripts/oracle --list-models --current openai-codex/gpt-5.6-sol
|
|
47
47
|
|
|
48
48
|
# Ask the automatically selected oracle model
|
|
49
|
-
./scripts/oracle --current openai-codex/gpt-5.
|
|
49
|
+
./scripts/oracle --current openai-codex/gpt-5.6-sol \
|
|
50
50
|
-p "<task>" --file "src/**" --file "!**/*.test.*"
|
|
51
51
|
|
|
52
52
|
# Override the oracle model when the automatic choice is wrong
|
|
53
|
-
./scripts/oracle --model
|
|
53
|
+
./scripts/oracle --model anthropic/claude-fable-5 \
|
|
54
54
|
-p "<task>" --file "src/**" --file "!**/*.test.*"
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -144,82 +144,86 @@ function selectOracleModel(models, enabledModels, currentFamily) {
|
|
|
144
144
|
|
|
145
145
|
function bestModelForFamily(models, enabledModels, family) {
|
|
146
146
|
return models
|
|
147
|
-
.
|
|
148
|
-
.
|
|
149
|
-
.sort((a, b) => b.score - a.score || a.model.spec.localeCompare(b.model.spec))[0]?.model;
|
|
147
|
+
.filter((model) => modelFamily(model.spec) === family)
|
|
148
|
+
.sort((left, right) => compareModels(left, right, enabledModels, family))[0];
|
|
150
149
|
}
|
|
151
150
|
|
|
152
|
-
function
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
151
|
+
function compareModels(left, right, enabledModels, family) {
|
|
152
|
+
return (
|
|
153
|
+
Number(enabledModels.has(normalizeModelSpec(right.spec))) -
|
|
154
|
+
Number(enabledModels.has(normalizeModelSpec(left.spec))) ||
|
|
155
|
+
modelRank(right.model, family) - modelRank(left.model, family) ||
|
|
156
|
+
providerRank(right.provider, family) - providerRank(left.provider, family) ||
|
|
157
|
+
left.spec.localeCompare(right.spec)
|
|
158
|
+
);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
function
|
|
161
|
+
function providerRank(provider, family) {
|
|
162
162
|
const normalized = provider.toLowerCase();
|
|
163
163
|
if (family === "openai") {
|
|
164
|
-
if (normalized === "openai-codex") return
|
|
165
|
-
if (normalized === "openai") return
|
|
166
|
-
if (normalized === "github-copilot") return
|
|
167
|
-
if (normalized.includes("azure")) return
|
|
168
|
-
if (normalized === "openrouter") return
|
|
169
|
-
return
|
|
164
|
+
if (normalized === "openai-codex") return 5;
|
|
165
|
+
if (normalized === "openai") return 4;
|
|
166
|
+
if (normalized === "github-copilot") return 3;
|
|
167
|
+
if (normalized.includes("azure")) return 2;
|
|
168
|
+
if (normalized === "openrouter") return 1;
|
|
169
|
+
return 0;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
if (family === "anthropic") {
|
|
173
|
-
if (normalized === "github-copilot") return
|
|
174
|
-
if (normalized === "anthropic") return
|
|
175
|
-
if (normalized === "amazon-bedrock") return
|
|
176
|
-
if (normalized === "openrouter") return
|
|
177
|
-
return
|
|
173
|
+
if (normalized === "github-copilot") return 4;
|
|
174
|
+
if (normalized === "anthropic") return 3;
|
|
175
|
+
if (normalized === "amazon-bedrock") return 2;
|
|
176
|
+
if (normalized === "openrouter") return 1;
|
|
177
|
+
return 0;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
if (family === "google") {
|
|
181
|
-
if (normalized === "google") return
|
|
182
|
-
if (normalized === "github-copilot") return
|
|
183
|
-
if (normalized.includes("vertex")) return
|
|
184
|
-
if (normalized === "openrouter") return
|
|
185
|
-
return
|
|
181
|
+
if (normalized === "google") return 4;
|
|
182
|
+
if (normalized === "github-copilot") return 3;
|
|
183
|
+
if (normalized.includes("vertex")) return 2;
|
|
184
|
+
if (normalized === "openrouter") return 1;
|
|
185
|
+
return 0;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
return 0;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
function
|
|
191
|
+
function modelRank(model, family) {
|
|
192
192
|
const normalized = model.toLowerCase();
|
|
193
193
|
|
|
194
194
|
if (family === "openai") {
|
|
195
|
-
if (/gpt-5\.
|
|
196
|
-
if (/gpt-5\.
|
|
197
|
-
if (/gpt-5\.
|
|
198
|
-
if (/gpt-5\.
|
|
199
|
-
if (/gpt-5\.
|
|
200
|
-
if (/gpt-5\.
|
|
201
|
-
if (/gpt-5/.test(normalized)) return
|
|
202
|
-
if (/gpt-
|
|
203
|
-
return
|
|
195
|
+
if (/gpt-5\.6[-_.]sol/.test(normalized)) return 10;
|
|
196
|
+
if (/gpt-5\.6/.test(normalized)) return 9;
|
|
197
|
+
if (/gpt-5\.5/.test(normalized)) return 8;
|
|
198
|
+
if (/gpt-5\.4/.test(normalized)) return 7;
|
|
199
|
+
if (/gpt-5\.3.*codex/.test(normalized)) return 6;
|
|
200
|
+
if (/gpt-5\.3/.test(normalized)) return 5;
|
|
201
|
+
if (/gpt-5\.2/.test(normalized)) return 4;
|
|
202
|
+
if (/gpt-5\.1/.test(normalized)) return 3;
|
|
203
|
+
if (/gpt-5/.test(normalized)) return 2;
|
|
204
|
+
if (/gpt-4\.1/.test(normalized)) return 1;
|
|
205
|
+
return 0;
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
if (family === "anthropic") {
|
|
207
|
-
if (/claude[-_.]
|
|
208
|
-
if (/claude[-_.]opus[-_.]4[-_.]
|
|
209
|
-
if (/claude[-_.]opus[-_.]4[-_.]
|
|
210
|
-
if (/claude[-_.]opus[-_.]4[-_.]
|
|
211
|
-
if (/claude[-_.]opus[-_.]4/.test(normalized)) return
|
|
212
|
-
if (/claude[-_.]
|
|
213
|
-
if (/claude[-_.]
|
|
214
|
-
if (/claude[-_.]sonnet[-_.]4/.test(normalized)) return
|
|
215
|
-
return
|
|
209
|
+
if (/claude[-_.]fable[-_.]5/.test(normalized)) return 10;
|
|
210
|
+
if (/claude[-_.]opus[-_.]4[-_.]8/.test(normalized)) return 9;
|
|
211
|
+
if (/claude[-_.]opus[-_.]4[-_.]7/.test(normalized)) return 8;
|
|
212
|
+
if (/claude[-_.]opus[-_.]4[-_.]6/.test(normalized)) return 7;
|
|
213
|
+
if (/claude[-_.]opus[-_.]4[-_.]5/.test(normalized)) return 6;
|
|
214
|
+
if (/claude[-_.]opus[-_.]4[-_.]1/.test(normalized)) return 5;
|
|
215
|
+
if (/claude[-_.]opus[-_.]4/.test(normalized)) return 4;
|
|
216
|
+
if (/claude[-_.]sonnet[-_.]4[-_.]6/.test(normalized)) return 3;
|
|
217
|
+
if (/claude[-_.]sonnet[-_.]4[-_.]5/.test(normalized)) return 2;
|
|
218
|
+
if (/claude[-_.]sonnet[-_.]4/.test(normalized)) return 1;
|
|
219
|
+
return 0;
|
|
216
220
|
}
|
|
217
221
|
|
|
218
222
|
if (family === "google") {
|
|
219
|
-
if (/gemini[-_.]3\.1[-_.]pro/.test(normalized)) return
|
|
220
|
-
if (/gemini[-_.]3[-_.]pro/.test(normalized)) return
|
|
221
|
-
if (/gemini[-_.]2\.5[-_.]pro/.test(normalized)) return
|
|
222
|
-
return
|
|
223
|
+
if (/gemini[-_.]3\.1[-_.]pro/.test(normalized)) return 3;
|
|
224
|
+
if (/gemini[-_.]3[-_.]pro/.test(normalized)) return 2;
|
|
225
|
+
if (/gemini[-_.]2\.5[-_.]pro/.test(normalized)) return 1;
|
|
226
|
+
return 0;
|
|
223
227
|
}
|
|
224
228
|
|
|
225
229
|
return 0;
|