@inneranimalmedia/agentsam-sdk 2.6.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/PLATFORM_RUNTIME_EVENTS.md +48 -0
- package/docs/RELEASES.md +7 -7
- package/docs/SOURCE_ARCHITECTURE.md +58 -0
- package/docs/TEST_TIERS.md +26 -0
- package/migrations/runtime/0001_cli_runtime.sql +298 -0
- package/package.json +28 -7
- package/packages/agentsam-repository/README.md +15 -0
- package/packages/agentsam-repository/package.json +25 -0
- package/packages/agentsam-repository/src/contracts.js +113 -0
- package/packages/agentsam-repository/src/index.js +3 -0
- package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
- package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
- package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
- package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
- package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
- package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
- package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
- package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
- package/packages/identity/package.json +1 -1
- package/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
- package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
- package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
- package/protocol/repository/repository-contract.schema.json +24 -0
- package/protocol/repository/repository-dependency.schema.json +24 -0
- package/protocol/repository/repository-identity.schema.json +17 -0
- package/protocol/rpc/v1/common.proto +16 -0
- package/protocol/rpc/v1/errors.proto +35 -0
- package/protocol/rpc/v1/knowledge.proto +77 -0
- package/services/knowledge/package-lock.json +333 -0
- package/services/knowledge/package.json +5 -1
- package/src/agent/responses-runner.js +63 -35
- package/src/capabilities/repository-snapshot.js +3 -3
- package/src/cli.js +23 -6
- package/src/commands/context-economics.js +17 -2
- package/src/commands/context.js +1 -1
- package/src/commands/db.js +20 -3
- package/src/commands/env.js +90 -0
- package/src/commands/knowledge.js +12 -4
- package/src/commands/merkle-persist.js +30 -11
- package/src/commands/merkle.js +1 -1
- package/src/commands/models.js +123 -65
- package/src/commands/ollama.js +26 -0
- package/src/commands/preferences.js +53 -26
- package/src/commands/shell.js +236 -48
- package/src/errors/contract.js +236 -0
- package/src/errors/index.js +14 -0
- package/src/index.js +13 -1
- package/src/knowledge/service/auth.js +13 -0
- package/src/knowledge/service/grpc-client.js +115 -0
- package/src/knowledge/service/grpc-codec.js +237 -0
- package/src/knowledge/service/grpc-server.js +83 -0
- package/src/knowledge/service/job-engine.js +248 -0
- package/src/knowledge/service/server.js +87 -135
- package/src/knowledge/source.js +1 -1
- package/src/lib/cli-preferences.js +31 -4
- package/src/lib/deploy-receipt/index.js +2 -2
- package/src/lib/knowledge-docker.js +6 -3
- package/src/lib/local-sessions.js +23 -2
- package/src/lib/local-status.js +1 -1
- package/src/lib/project-config.js +1 -1
- package/src/lib/provider-credentials.js +105 -5
- package/src/lib/slash-commands.js +4 -3
- package/src/local/migrations.js +93 -0
- package/src/local/runtime-store.js +141 -0
- package/src/local/sqlite.js +2 -0
- package/src/local-pty/server.js +113 -51
- package/src/models/discovery.js +292 -0
- package/src/providers/anthropic-messages.js +192 -0
- package/src/providers/cloudflare-chat.js +183 -0
- package/src/providers/factory.js +69 -0
- package/src/providers/gemini-generate-content.js +208 -0
- package/src/providers/index.js +5 -0
- package/src/providers/ollama-chat.js +148 -0
- package/src/providers/openai-responses.js +226 -75
- package/src/repository/index.js +14 -2
- package/src/rpc/generated/common_grpc_pb.js +1 -0
- package/src/rpc/generated/common_pb.js +536 -0
- package/src/rpc/generated/errors_grpc_pb.js +1 -0
- package/src/rpc/generated/errors_pb.js +482 -0
- package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
- package/src/rpc/generated/knowledge_pb.js +2168 -0
- package/src/rpc/generated/package.json +3 -0
- package/src/security/trust-boundary.js +2 -2
- package/src/telemetry/events.js +4 -1
- package/src/ui/cli/activity.js +76 -0
- package/src/ui/cli/compaction.js +15 -0
- package/src/ui/cli/footer.js +39 -0
- package/src/ui/cli/help.js +192 -0
- package/src/ui/cli/plan.js +20 -0
- package/src/ui/cli/runtime-events.js +110 -0
- package/src/ui/cli/waiting.js +16 -0
- package/src/ui/merkle/render.js +1 -1
- package/test/cli/preferences-runtime.test.mjs +11 -0
- package/test/cli/runtime-ui.test.mjs +74 -0
- package/test/error-diagnostics.test.mjs +57 -1
- package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
- package/test/integration/cli-help.test.mjs +37 -0
- package/test/integration/knowledge-rpc.test.mjs +112 -0
- package/test/integration/merkle-cli.test.mjs +61 -0
- package/test/integration/merkle-persistence-identity.test.mjs +48 -0
- package/test/integration/provider-env-cli.test.mjs +49 -0
- package/test/integration/provider-factory.test.mjs +197 -0
- package/test/integration/repository-company-graph.test.mjs +90 -0
- package/test/integration/runtime-migrations.test.mjs +82 -0
- package/test/knowledge-service.test.mjs +5 -0
- package/test/knowledge.test.mjs +16 -0
- package/test/live/terminal-transport.live.test.mjs +24 -0
- package/test/local-sessions.test.mjs +7 -1
- package/test/models.test.mjs +101 -4
- package/test/ollama.test.mjs +21 -0
- package/test/portable-context.test.mjs +1 -1
- package/test/provider-credentials.test.mjs +45 -1
- package/test/release-hygiene.test.mjs +13 -5
- package/test/responses-runner.test.mjs +3 -1
- package/test/shell.test.mjs +50 -8
- package/test/terminal/local-pty.mock.test.mjs +151 -0
- /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
package/src/local-pty/server.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* Agent Sam local PTY — localhost WebSocket shell, no tunnel, no IAM.
|
|
3
3
|
* Compatible with iam-pty wire format (raw bytes + JSON resize/slash).
|
|
4
4
|
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
5
6
|
import http from 'node:http';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
6
9
|
import { WebSocketServer } from 'ws';
|
|
7
10
|
|
|
8
11
|
const DEFAULT_PORT = 3099;
|
|
@@ -17,8 +20,45 @@ function shellForPlatform() {
|
|
|
17
20
|
return process.env.SHELL || '/bin/zsh';
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
export function ensureNodePtySpawnHelperExecutable(options = {}) {
|
|
24
|
+
const platform = options.platform || process.platform;
|
|
25
|
+
const arch = options.arch || process.arch;
|
|
26
|
+
if (platform !== 'darwin') return Object.freeze({ checked: false, changed: false, path: null });
|
|
27
|
+
|
|
28
|
+
const resolveModule = options.resolveModule || ((specifier) => import.meta.resolve(specifier));
|
|
29
|
+
const fsImpl = options.fs || fs;
|
|
30
|
+
const entryUrl = resolveModule('node-pty');
|
|
31
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(entryUrl)), '..');
|
|
32
|
+
const helperPath = path.join(packageRoot, 'prebuilds', `darwin-${arch}`, 'spawn-helper');
|
|
33
|
+
|
|
34
|
+
if (!fsImpl.existsSync(helperPath)) {
|
|
35
|
+
return Object.freeze({ checked: true, changed: false, path: helperPath, missing: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const stat = fsImpl.statSync(helperPath);
|
|
39
|
+
if ((stat.mode & 0o111) !== 0) {
|
|
40
|
+
return Object.freeze({ checked: true, changed: false, path: helperPath });
|
|
41
|
+
}
|
|
42
|
+
|
|
21
43
|
try {
|
|
44
|
+
fsImpl.chmodSync(helperPath, stat.mode | 0o111);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const wrapped = new Error(
|
|
47
|
+
`node-pty spawn-helper is not executable and Agent Sam could not repair it at ${helperPath}. ` +
|
|
48
|
+
`Reinstall node-pty with install scripts enabled or make that helper executable. ${error?.message || error}`,
|
|
49
|
+
);
|
|
50
|
+
wrapped.code = 'node_pty_spawn_helper_not_executable';
|
|
51
|
+
wrapped.cause = error;
|
|
52
|
+
throw wrapped;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return Object.freeze({ checked: true, changed: true, path: helperPath });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function loadPty(override) {
|
|
59
|
+
if (override?.spawn) return override;
|
|
60
|
+
try {
|
|
61
|
+
ensureNodePtySpawnHelperExecutable();
|
|
22
62
|
const mod = await import('node-pty');
|
|
23
63
|
return mod.default || mod;
|
|
24
64
|
} catch (e) {
|
|
@@ -29,12 +69,69 @@ async function loadPty() {
|
|
|
29
69
|
}
|
|
30
70
|
|
|
31
71
|
/**
|
|
32
|
-
*
|
|
72
|
+
* Attach the portable PTY wire protocol to an already-created WebSocket-like transport.
|
|
73
|
+
* Exported so the release gate can test terminal semantics against an in-memory mock transport.
|
|
74
|
+
*/
|
|
75
|
+
export function attachLocalPtySession({ ws, pty, shell, cwd, cols = 80, rows = 24, env = process.env, sessionId } = {}) {
|
|
76
|
+
if (!ws?.on || !ws?.send) throw new TypeError('ws transport with on/send is required');
|
|
77
|
+
if (!pty?.spawn) throw new TypeError('pty transport with spawn is required');
|
|
78
|
+
const term = pty.spawn(shell, [], {
|
|
79
|
+
name: 'xterm-256color',
|
|
80
|
+
cols,
|
|
81
|
+
rows,
|
|
82
|
+
cwd,
|
|
83
|
+
env: { ...env, TERM: 'xterm-256color', AGENTSAM_LOCAL_PTY: '1' },
|
|
84
|
+
});
|
|
85
|
+
const id = sessionId || `local_${Date.now().toString(36)}`;
|
|
86
|
+
const openState = ws.OPEN ?? 1;
|
|
87
|
+
const isOpen = () => ws.readyState == null || ws.readyState === openState;
|
|
88
|
+
|
|
89
|
+
ws.send(JSON.stringify({ type: 'session_id', session_id: id }));
|
|
90
|
+
|
|
91
|
+
term.onData((data) => {
|
|
92
|
+
if (isOpen()) ws.send(data);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
ws.on('message', (raw) => {
|
|
96
|
+
const text = raw.toString();
|
|
97
|
+
try {
|
|
98
|
+
const msg = JSON.parse(text);
|
|
99
|
+
if (msg.type === 'resize' && msg.cols && msg.rows) {
|
|
100
|
+
term.resize(msg.cols, msg.rows);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (msg.type === 'slash' && msg.line) {
|
|
104
|
+
term.write(`${msg.line}\r`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
/* raw PTY input */
|
|
109
|
+
}
|
|
110
|
+
term.write(text);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
let cleaned = false;
|
|
114
|
+
const cleanup = () => {
|
|
115
|
+
if (cleaned) return;
|
|
116
|
+
cleaned = true;
|
|
117
|
+
try { term.kill(); } catch { /* ignore */ }
|
|
118
|
+
};
|
|
119
|
+
term.onExit(() => {
|
|
120
|
+
if (isOpen() && ws.close) ws.close();
|
|
121
|
+
});
|
|
122
|
+
ws.on('close', cleanup);
|
|
123
|
+
ws.on('error', cleanup);
|
|
124
|
+
|
|
125
|
+
return Object.freeze({ session_id: id, term, cleanup });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {{ cwd?: string, port?: number, host?: string, pty?: { spawn: Function } }} [opts]
|
|
33
130
|
*/
|
|
34
131
|
export async function startLocalPtyServer(opts = {}) {
|
|
35
|
-
const pty = await loadPty();
|
|
132
|
+
const pty = await loadPty(opts.pty);
|
|
36
133
|
const cwd = opts.cwd || process.cwd();
|
|
37
|
-
const
|
|
134
|
+
const requestedPort = opts.port === 0 ? 0 : parsePort(opts.port ?? process.env.PTY_PORT, DEFAULT_PORT);
|
|
38
135
|
const host = opts.host || '127.0.0.1';
|
|
39
136
|
const shell = shellForPlatform();
|
|
40
137
|
|
|
@@ -47,7 +144,7 @@ export async function startLocalPtyServer(opts = {}) {
|
|
|
47
144
|
ok: true,
|
|
48
145
|
service: 'agentsam-local-pty',
|
|
49
146
|
cwd,
|
|
50
|
-
port,
|
|
147
|
+
port: Number(httpServer.address()?.port || requestedPort),
|
|
51
148
|
shell,
|
|
52
149
|
}),
|
|
53
150
|
);
|
|
@@ -65,64 +162,29 @@ export async function startLocalPtyServer(opts = {}) {
|
|
|
65
162
|
const cols = parsePort(url.searchParams.get('cols'), 80);
|
|
66
163
|
const rows = parsePort(url.searchParams.get('rows'), 24);
|
|
67
164
|
|
|
68
|
-
|
|
69
|
-
|
|
165
|
+
attachLocalPtySession({
|
|
166
|
+
ws,
|
|
167
|
+
pty,
|
|
168
|
+
shell,
|
|
169
|
+
cwd: sessionCwd,
|
|
70
170
|
cols,
|
|
71
171
|
rows,
|
|
72
|
-
|
|
73
|
-
env: { ...process.env, TERM: 'xterm-256color', AGENTSAM_LOCAL_PTY: '1' },
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const sessionId = `local_${Date.now().toString(36)}`;
|
|
77
|
-
ws.send(JSON.stringify({ type: 'session_id', session_id: sessionId }));
|
|
78
|
-
|
|
79
|
-
term.onData((data) => {
|
|
80
|
-
if (ws.readyState === ws.OPEN) ws.send(data);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
ws.on('message', (raw) => {
|
|
84
|
-
const text = raw.toString();
|
|
85
|
-
try {
|
|
86
|
-
const msg = JSON.parse(text);
|
|
87
|
-
if (msg.type === 'resize' && msg.cols && msg.rows) {
|
|
88
|
-
term.resize(msg.cols, msg.rows);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (msg.type === 'slash' && msg.line) {
|
|
92
|
-
term.write(`${msg.line}\r`);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
} catch {
|
|
96
|
-
/* raw PTY input */
|
|
97
|
-
}
|
|
98
|
-
term.write(text);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const cleanup = () => {
|
|
102
|
-
try {
|
|
103
|
-
term.kill();
|
|
104
|
-
} catch {
|
|
105
|
-
/* ignore */
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
term.onExit(() => {
|
|
109
|
-
if (ws.readyState === ws.OPEN) ws.close();
|
|
172
|
+
env: process.env,
|
|
110
173
|
});
|
|
111
|
-
ws.on('close', cleanup);
|
|
112
|
-
ws.on('error', cleanup);
|
|
113
174
|
});
|
|
114
175
|
|
|
115
176
|
await new Promise((resolve) => {
|
|
116
|
-
httpServer.listen(
|
|
177
|
+
httpServer.listen(requestedPort, host, resolve);
|
|
117
178
|
});
|
|
179
|
+
const boundPort = Number(httpServer.address()?.port || requestedPort);
|
|
118
180
|
|
|
119
181
|
return {
|
|
120
|
-
port,
|
|
182
|
+
port: boundPort,
|
|
121
183
|
host,
|
|
122
184
|
cwd,
|
|
123
185
|
shell,
|
|
124
|
-
url: `ws://${host}:${
|
|
125
|
-
healthUrl: `http://${host}:${
|
|
186
|
+
url: `ws://${host}:${boundPort}`,
|
|
187
|
+
healthUrl: `http://${host}:${boundPort}/health`,
|
|
126
188
|
close: () =>
|
|
127
189
|
new Promise((resolve, reject) => {
|
|
128
190
|
wss.close(() => {
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { getModelRecord } from './catalog.js';
|
|
2
|
+
|
|
3
|
+
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
4
|
+
function positiveInt(value) {
|
|
5
|
+
const n = Number(value);
|
|
6
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
7
|
+
}
|
|
8
|
+
function timeoutSignal(ms = 8_000) {
|
|
9
|
+
return typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(ms) : undefined;
|
|
10
|
+
}
|
|
11
|
+
function failure(error, attempted = true) {
|
|
12
|
+
return { attempted, ok: false, models: [], error: error?.message || String(error || 'unknown error') };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function providerReference(provider, id) {
|
|
16
|
+
if (provider !== 'anthropic') return null;
|
|
17
|
+
const million = new Set([
|
|
18
|
+
'claude-fable-5', 'claude-opus-5', 'claude-opus-4-8', 'claude-opus-4-7',
|
|
19
|
+
'claude-opus-4-6', 'claude-sonnet-5', 'claude-sonnet-4-6',
|
|
20
|
+
]);
|
|
21
|
+
const twoHundredK = new Set([
|
|
22
|
+
'claude-opus-4-5-20251101', 'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001',
|
|
23
|
+
]);
|
|
24
|
+
const contextWindow = million.has(id) ? 1_000_000 : twoHundredK.has(id) ? 200_000 : null;
|
|
25
|
+
if (!contextWindow) return null;
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
model_key: `anthropic:${id}`,
|
|
28
|
+
provider: 'anthropic',
|
|
29
|
+
provider_model_id: id,
|
|
30
|
+
label: id,
|
|
31
|
+
context_window: contextWindow,
|
|
32
|
+
max_output_tokens: ['claude-opus-5', 'claude-sonnet-5'].includes(id) ? 128_000 : null,
|
|
33
|
+
reasoning_efforts: Object.freeze(['auto']),
|
|
34
|
+
service_tiers: Object.freeze(['default']),
|
|
35
|
+
capabilities: Object.freeze({ messages: true, function_calling: true, prompt_caching: true, compaction: true }),
|
|
36
|
+
source: Object.freeze({
|
|
37
|
+
url: 'https://docs.anthropic.com/en/docs/about-claude/models/overview',
|
|
38
|
+
as_of: '2026-09-17',
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fallbackRecord(provider, id) {
|
|
44
|
+
const record = getModelRecord(`${provider}:${id}`) || getModelRecord(id) || providerReference(provider, id);
|
|
45
|
+
return record || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function baseRecord(provider, id, values = {}) {
|
|
49
|
+
const fallback = fallbackRecord(provider, id);
|
|
50
|
+
const contextWindow = positiveInt(values.context_window ?? values.contextWindow ?? fallback?.context_window);
|
|
51
|
+
const maxOutput = positiveInt(values.max_output_tokens ?? values.maxOutputTokens ?? fallback?.max_output_tokens);
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
model_key: `${provider}:${id}`,
|
|
54
|
+
provider,
|
|
55
|
+
provider_model_id: id,
|
|
56
|
+
label: clean(values.label) || fallback?.label || id,
|
|
57
|
+
availability: 'available',
|
|
58
|
+
availability_source: 'provider_api',
|
|
59
|
+
context_window: contextWindow,
|
|
60
|
+
context_window_source: values.context_window != null || values.contextWindow != null
|
|
61
|
+
? 'provider_api'
|
|
62
|
+
: fallback?.context_window ? 'sdk_reference' : 'unknown',
|
|
63
|
+
max_output_tokens: maxOutput,
|
|
64
|
+
max_output_tokens_source: values.max_output_tokens != null || values.maxOutputTokens != null
|
|
65
|
+
? 'provider_api'
|
|
66
|
+
: fallback?.max_output_tokens ? 'sdk_reference' : 'unknown',
|
|
67
|
+
reasoning_efforts: Object.freeze(
|
|
68
|
+
Array.isArray(values.reasoning_efforts) && values.reasoning_efforts.length
|
|
69
|
+
? [...values.reasoning_efforts]
|
|
70
|
+
: fallback?.reasoning_efforts ? [...fallback.reasoning_efforts] : ['auto'],
|
|
71
|
+
),
|
|
72
|
+
service_tiers: Object.freeze(
|
|
73
|
+
Array.isArray(values.service_tiers) && values.service_tiers.length
|
|
74
|
+
? [...values.service_tiers]
|
|
75
|
+
: fallback?.service_tiers ? [...fallback.service_tiers] : ['default'],
|
|
76
|
+
),
|
|
77
|
+
capabilities: Object.freeze({
|
|
78
|
+
...(fallback?.capabilities || {}),
|
|
79
|
+
...(values.capabilities || {}),
|
|
80
|
+
}),
|
|
81
|
+
pricing: values.pricing || fallback?.pricing || null,
|
|
82
|
+
context_policy: values.context_policy || fallback?.context_policy || null,
|
|
83
|
+
batch: values.batch || fallback?.batch || null,
|
|
84
|
+
source: Object.freeze({
|
|
85
|
+
kind: 'provider_api',
|
|
86
|
+
url: clean(values.source_url) || null,
|
|
87
|
+
discovered_at: new Date().toISOString(),
|
|
88
|
+
fallback: fallback?.source || null,
|
|
89
|
+
}),
|
|
90
|
+
metadata: Object.freeze(values.metadata && typeof values.metadata === 'object' ? { ...values.metadata } : {}),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function fetchJson(fetchImpl, url, init) {
|
|
95
|
+
const response = await fetchImpl(url, { ...init, signal: init?.signal || timeoutSignal() });
|
|
96
|
+
let body = null;
|
|
97
|
+
try { body = await response.json(); } catch { body = null; }
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
const message = clean(body?.error?.message || body?.message || body?.errors?.[0]?.message) || `HTTP ${response.status}`;
|
|
100
|
+
const error = new Error(message);
|
|
101
|
+
error.status = response.status;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
return body;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function discoverOpenAIModels(apiKey, fetchImpl = fetch) {
|
|
108
|
+
if (!clean(apiKey)) return failure('credential unavailable', false);
|
|
109
|
+
try {
|
|
110
|
+
const body = await fetchJson(fetchImpl, 'https://api.openai.com/v1/models', {
|
|
111
|
+
headers: { authorization: `Bearer ${clean(apiKey)}` },
|
|
112
|
+
});
|
|
113
|
+
const models = (Array.isArray(body?.data) ? body.data : [])
|
|
114
|
+
.map((row) => clean(row?.id))
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.map((id) => baseRecord('openai', id, {
|
|
117
|
+
source_url: 'https://api.openai.com/v1/models',
|
|
118
|
+
capabilities: { responses: true },
|
|
119
|
+
}));
|
|
120
|
+
return { attempted: true, ok: true, models, error: null };
|
|
121
|
+
} catch (error) { return failure(error); }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function discoverAnthropicModels(apiKey, fetchImpl = fetch) {
|
|
125
|
+
if (!clean(apiKey)) return failure('credential unavailable', false);
|
|
126
|
+
try {
|
|
127
|
+
const body = await fetchJson(fetchImpl, 'https://api.anthropic.com/v1/models?limit=1000', {
|
|
128
|
+
headers: {
|
|
129
|
+
'x-api-key': clean(apiKey),
|
|
130
|
+
'anthropic-version': '2023-06-01',
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const models = (Array.isArray(body?.data) ? body.data : [])
|
|
134
|
+
.map((row) => {
|
|
135
|
+
const id = clean(row?.id);
|
|
136
|
+
if (!id) return null;
|
|
137
|
+
return baseRecord('anthropic', id, {
|
|
138
|
+
label: clean(row?.display_name) || id,
|
|
139
|
+
source_url: 'https://api.anthropic.com/v1/models',
|
|
140
|
+
capabilities: { messages: true, function_calling: true, prompt_caching: true },
|
|
141
|
+
metadata: { created_at: row?.created_at || null, type: row?.type || null },
|
|
142
|
+
});
|
|
143
|
+
})
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
return { attempted: true, ok: true, models, error: null };
|
|
146
|
+
} catch (error) { return failure(error); }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function discoverGeminiModels(apiKey, fetchImpl = fetch) {
|
|
150
|
+
if (!clean(apiKey)) return failure('credential unavailable', false);
|
|
151
|
+
try {
|
|
152
|
+
const body = await fetchJson(
|
|
153
|
+
fetchImpl,
|
|
154
|
+
`https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000&key=${encodeURIComponent(clean(apiKey))}`,
|
|
155
|
+
{},
|
|
156
|
+
);
|
|
157
|
+
const models = (Array.isArray(body?.models) ? body.models : [])
|
|
158
|
+
.filter((row) => (row?.supportedGenerationMethods || []).includes('generateContent'))
|
|
159
|
+
.map((row) => {
|
|
160
|
+
const id = clean(row?.baseModelId || row?.name).replace(/^models\//, '');
|
|
161
|
+
if (!id) return null;
|
|
162
|
+
return baseRecord('gemini', id, {
|
|
163
|
+
label: clean(row?.displayName) || id,
|
|
164
|
+
context_window: positiveInt(row?.inputTokenLimit),
|
|
165
|
+
max_output_tokens: positiveInt(row?.outputTokenLimit),
|
|
166
|
+
reasoning_efforts: row?.thinking === true ? ['auto', 'low', 'medium', 'high'] : ['auto'],
|
|
167
|
+
source_url: 'https://generativelanguage.googleapis.com/v1beta/models',
|
|
168
|
+
capabilities: {
|
|
169
|
+
generate_content: true,
|
|
170
|
+
function_calling: true,
|
|
171
|
+
thinking: row?.thinking === true,
|
|
172
|
+
},
|
|
173
|
+
metadata: {
|
|
174
|
+
version: row?.version || null,
|
|
175
|
+
supported_generation_methods: row?.supportedGenerationMethods || [],
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
})
|
|
179
|
+
.filter(Boolean);
|
|
180
|
+
return { attempted: true, ok: true, models, error: null };
|
|
181
|
+
} catch (error) { return failure(error); }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function xaiPricing(row) {
|
|
185
|
+
const input = Number(row?.prompt_text_token_price);
|
|
186
|
+
const cached = Number(row?.cached_prompt_text_token_price);
|
|
187
|
+
const output = Number(row?.completion_text_token_price);
|
|
188
|
+
if (![input, output].every(Number.isFinite)) return null;
|
|
189
|
+
// xAI model API prices are returned in nanos per token. Convert to USD / million tokens.
|
|
190
|
+
const nanosToPerMillionUsd = (nanos) => Number.isFinite(nanos) ? nanos / 1000 : 0;
|
|
191
|
+
return Object.freeze({
|
|
192
|
+
currency: 'USD',
|
|
193
|
+
unit: 'per_million_tokens',
|
|
194
|
+
input: nanosToPerMillionUsd(input),
|
|
195
|
+
cached_input: nanosToPerMillionUsd(cached),
|
|
196
|
+
cache_write: nanosToPerMillionUsd(input),
|
|
197
|
+
output: nanosToPerMillionUsd(output),
|
|
198
|
+
thresholds: Object.freeze(
|
|
199
|
+
Number.isFinite(Number(row?.long_context_threshold))
|
|
200
|
+
? [Object.freeze({
|
|
201
|
+
input_tokens_gt: Number(row.long_context_threshold),
|
|
202
|
+
applies_to_full_request: true,
|
|
203
|
+
multipliers: Object.freeze({
|
|
204
|
+
input: Number(row?.prompt_text_token_price_long_context) / input || 1,
|
|
205
|
+
cached_input: 1,
|
|
206
|
+
cache_write: Number(row?.prompt_text_token_price_long_context) / input || 1,
|
|
207
|
+
output: Number(row?.completion_text_token_price_long_context) / output || 1,
|
|
208
|
+
}),
|
|
209
|
+
})]
|
|
210
|
+
: [],
|
|
211
|
+
),
|
|
212
|
+
service_tier_multipliers: Object.freeze({ default: 1 }),
|
|
213
|
+
source: 'https://api.x.ai/v1/models',
|
|
214
|
+
as_of: new Date().toISOString().slice(0, 10),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function discoverXaiModels(apiKey, fetchImpl = fetch) {
|
|
219
|
+
if (!clean(apiKey)) return failure('credential unavailable', false);
|
|
220
|
+
try {
|
|
221
|
+
const body = await fetchJson(fetchImpl, 'https://api.x.ai/v1/models', {
|
|
222
|
+
headers: { authorization: `Bearer ${clean(apiKey)}` },
|
|
223
|
+
});
|
|
224
|
+
const models = (Array.isArray(body?.data) ? body.data : [])
|
|
225
|
+
.map((row) => {
|
|
226
|
+
const id = clean(row?.id);
|
|
227
|
+
if (!id || /image|video|voice|embedding/i.test(id)) return null;
|
|
228
|
+
return baseRecord('grok', id, {
|
|
229
|
+
context_window: positiveInt(row?.context_length),
|
|
230
|
+
pricing: xaiPricing(row),
|
|
231
|
+
source_url: 'https://api.x.ai/v1/models',
|
|
232
|
+
capabilities: { responses: true, function_calling: true, prompt_caching: true },
|
|
233
|
+
metadata: {
|
|
234
|
+
aliases: row?.aliases || [],
|
|
235
|
+
created: row?.created || null,
|
|
236
|
+
owned_by: row?.owned_by || null,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
})
|
|
240
|
+
.filter(Boolean);
|
|
241
|
+
return { attempted: true, ok: true, models, error: null };
|
|
242
|
+
} catch (error) { return failure(error); }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function cloudflareTaskName(task) {
|
|
246
|
+
if (typeof task === 'string') return clean(task);
|
|
247
|
+
if (task && typeof task === 'object') return clean(task.name || task.id);
|
|
248
|
+
return '';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function discoverCloudflareModels(apiToken, accountId, fetchImpl = fetch) {
|
|
252
|
+
if (!clean(apiToken)) return failure('credential unavailable', false);
|
|
253
|
+
if (!clean(accountId)) return failure('ACCOUNT_ID is required for Workers AI discovery');
|
|
254
|
+
try {
|
|
255
|
+
const url = `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(clean(accountId))}/ai/models/search`;
|
|
256
|
+
const body = await fetchJson(fetchImpl, url, {
|
|
257
|
+
headers: { authorization: `Bearer ${clean(apiToken)}` },
|
|
258
|
+
});
|
|
259
|
+
if (body?.success === false) throw new Error(clean(body?.errors?.[0]?.message) || 'Cloudflare API error');
|
|
260
|
+
const models = (Array.isArray(body?.result) ? body.result : [])
|
|
261
|
+
.map((row) => {
|
|
262
|
+
const id = clean(row?.name);
|
|
263
|
+
const task = cloudflareTaskName(row?.task);
|
|
264
|
+
if (!id || task.toLowerCase() !== 'text generation') return null;
|
|
265
|
+
return baseRecord('cloudflare', id, {
|
|
266
|
+
label: id,
|
|
267
|
+
source_url: url,
|
|
268
|
+
capabilities: { workers_ai: true },
|
|
269
|
+
metadata: {
|
|
270
|
+
task,
|
|
271
|
+
author: clean(row?.author) || null,
|
|
272
|
+
description: clean(row?.description) || null,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
})
|
|
276
|
+
.filter(Boolean);
|
|
277
|
+
return { attempted: true, ok: true, models, error: null, account_id: clean(accountId) };
|
|
278
|
+
} catch (error) { return failure(error); }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function discoverProviderModels(provider, credential, options = {}) {
|
|
282
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
283
|
+
switch (clean(provider).toLowerCase()) {
|
|
284
|
+
case 'openai': return discoverOpenAIModels(credential?.value, fetchImpl);
|
|
285
|
+
case 'anthropic': return discoverAnthropicModels(credential?.value, fetchImpl);
|
|
286
|
+
case 'gemini': return discoverGeminiModels(credential?.value, fetchImpl);
|
|
287
|
+
case 'grok':
|
|
288
|
+
case 'xai': return discoverXaiModels(credential?.value, fetchImpl);
|
|
289
|
+
case 'cloudflare': return discoverCloudflareModels(credential?.value, credential?.account_id, fetchImpl);
|
|
290
|
+
default: return failure(`unsupported provider: ${provider}`, false);
|
|
291
|
+
}
|
|
292
|
+
}
|