@coda-console/claude 1.5.0 → 1.5.2
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/agent/last-listed.mjs +34 -0
- package/agent/providers.mjs +19 -12
- package/agent/session.mjs +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The same file in the Claude, Codex and OpenCode plugins: an agent plugin imports nothing of another's.
|
|
2
|
+
// What a model service listed when it last answered, kept in the plugin's state directory (CODA_PLUGIN_STATE) under the
|
|
3
|
+
// service's name. One that cannot be asked now (its machine restarting, the network to it down) is listed as it last said
|
|
4
|
+
// rather than left out of the session's models, and of Coda's catalog of them for as long as that is kept; a request to it
|
|
5
|
+
// fails then as any request to it would. One never listed here, or without a state directory, keeps its asking's error.
|
|
6
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const file = (state, ref) => join(state, 'service-models', `${ref.replaceAll('/', '.')}.json`);
|
|
10
|
+
const readText = path => { try { return readFileSync(path, 'utf8'); } catch { return null; } };
|
|
11
|
+
const kept = path => { try { const models = JSON.parse(readText(path)); return Array.isArray(models) ? models : null; } catch { return null; } };
|
|
12
|
+
|
|
13
|
+
// `ask()`: the models the service lists now.
|
|
14
|
+
export async function lastListed(ref, ask, state, log) {
|
|
15
|
+
let models;
|
|
16
|
+
try { models = await ask(); } catch (error) {
|
|
17
|
+
const last = state ? kept(file(state, ref)) : null;
|
|
18
|
+
if (!last) throw error;
|
|
19
|
+
log(`模型服务 ${ref} 列不出模型(${error.message}),沿用上次列出的 ${last.length} 个`);
|
|
20
|
+
return last;
|
|
21
|
+
}
|
|
22
|
+
if (state) remember(file(state, ref), JSON.stringify(models));
|
|
23
|
+
return models;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Written when it is not that already, whole: another session's process may read it meanwhile.
|
|
27
|
+
function remember(path, text) {
|
|
28
|
+
if (readText(path) === text) return;
|
|
29
|
+
try {
|
|
30
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
31
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
32
|
+
writeFileSync(temporary, text); renameSync(temporary, path);
|
|
33
|
+
} catch { /* Not kept: what the service listed still stands. */ }
|
|
34
|
+
}
|
package/agent/providers.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// its API (`serviceSettings`), set for that session's process only.
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
6
6
|
import { authOverrides } from './auth.mjs';
|
|
7
|
+
import { lastListed } from './last-listed.mjs';
|
|
7
8
|
|
|
8
9
|
export const SETTING = 'claude.modelProviders';
|
|
9
10
|
const log = text => process.stderr.write(`[claude-agent] ${text}\n`);
|
|
@@ -25,15 +26,20 @@ export function chosenServices(env = process.env) {
|
|
|
25
26
|
});
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
// The models a service lists, asked as an Anthropic client asks (Coda's「检查可用模型」)
|
|
29
|
-
// model, in the service's group. They have no
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
// The models a service lists, asked as an Anthropic client asks (Coda's「检查可用模型」), or as it last listed them when
|
|
30
|
+
// it cannot be asked now (last-listed.mjs): `<服务>/<模型>`, named by the model, in the service's group. They have no
|
|
31
|
+
// effort to choose.
|
|
32
|
+
export async function serviceModels({ ref, title, url, key }, fetcher = fetch, state) {
|
|
33
|
+
const listed = await lastListed(ref, async () => {
|
|
34
|
+
const at = `${url.replace(/\/+$/, '')}/models?limit=1000`;
|
|
35
|
+
const response = await fetcher(at, { headers: key ? { 'x-api-key': key, 'anthropic-version': '2023-06-01' } : {}, signal: AbortSignal.timeout(LIST_TIMEOUT) });
|
|
36
|
+
if (!response.ok) throw new Error(`${at} 回答 ${response.status}`);
|
|
37
|
+
const body = await response.json();
|
|
38
|
+
// Only a list replaces the one kept: an answer of another shape is the service not answering.
|
|
39
|
+
if (!Array.isArray(body?.data)) throw new Error(`${at} 的回答不是模型列表`);
|
|
40
|
+
return body.data.filter(model => typeof model?.id === 'string' && model.id);
|
|
41
|
+
}, state, log);
|
|
42
|
+
return listed.map(model => ({ id: `${ref}/${model.id}`, name: model.id, efforts: [], model: model.id, service: ref, group: title }));
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
// The flag settings Claude Code runs a service's model with (above the machine's and the project's settings, its environment
|
|
@@ -46,10 +52,11 @@ export function serviceSettings({ url, key }, model) {
|
|
|
46
52
|
CLAUDE_CODE_SUBAGENT_MODEL: model } };
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
// The services' models, listed with Claude's own in the order the services are chosen: none of one that cannot list them
|
|
50
|
-
|
|
55
|
+
// The services' models, listed with Claude's own in the order the services are chosen: none of one that cannot list them
|
|
56
|
+
// now and never did (`state`: the plugin's state directory).
|
|
57
|
+
export async function listedModels(services, state) {
|
|
51
58
|
return (await Promise.all(services.map(async service => {
|
|
52
|
-
try { return await serviceModels(service); }
|
|
59
|
+
try { return await serviceModels(service, fetch, state); }
|
|
53
60
|
catch (e) { log(`模型服务 ${service.ref} 列不出模型:${e.message}`); return []; }
|
|
54
61
|
}))).flat();
|
|
55
62
|
}
|
package/agent/session.mjs
CHANGED
|
@@ -146,7 +146,7 @@ export class ClaudeSession {
|
|
|
146
146
|
onElicitation: (request, context) => this.elicitation(request, context),
|
|
147
147
|
stderr: data => process.stderr.write(data),
|
|
148
148
|
};
|
|
149
|
-
const listed = listedModels(this.services);
|
|
149
|
+
const listed = listedModels(this.services, this.env.CODA_PLUGIN_STATE);
|
|
150
150
|
this.start(resume ? { resume } : { sessionId: this.sessionId });
|
|
151
151
|
const initialization = await this.initialized(), served = await listed;
|
|
152
152
|
if (this.phase !== 'starting') throw failure('Claude 会话在初始化时结束', -32001);
|
package/package.json
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
"id": "claude",
|
|
4
4
|
"displayName": "Claude",
|
|
5
5
|
"description": "Claude SDK Agent:Coda 基于 Claude Agent SDK 写的 ACP 入口,一个进程一个 SDK 会话;资源页的「Claude」面板显示会话报告的额度,可从 Coda 登录。",
|
|
6
|
-
"version": "1.5.
|
|
6
|
+
"version": "1.5.2",
|
|
7
|
+
"categories": ["Agent"],
|
|
7
8
|
"dependencies": {
|
|
8
9
|
"@agentclientprotocol/sdk": "1.4.0",
|
|
9
10
|
"@anthropic-ai/claude-agent-sdk": "0.3.296"
|