@coda-console/codex 1.8.1 → 1.8.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 +20 -13
- package/package.json +1 -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
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// configuration is not touched.
|
|
8
8
|
import { existsSync, readFileSync } from 'node:fs';
|
|
9
9
|
import { carriedInstructions } from './instructions.mjs';
|
|
10
|
+
import { lastListed } from './last-listed.mjs';
|
|
10
11
|
|
|
11
12
|
export const SETTING = 'codex.modelProviders';
|
|
12
13
|
const log = text => process.stderr.write(`[codex-agent] ${text}\n`);
|
|
@@ -28,15 +29,20 @@ export function chosenServices(env = process.env) {
|
|
|
28
29
|
});
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
// The models a service lists,
|
|
32
|
-
// model, with no effort or speed to choose; `codaService` and
|
|
33
|
-
// the model's group.
|
|
34
|
-
export async function serviceModels({ ref, title, url, key }, fetcher = fetch) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
// The models a service lists, or last listed when it cannot be asked now (last-listed.mjs), as Codex's model list has
|
|
33
|
+
// them (app-server `model/list`): `<服务>/<模型>`, named by the model, with no effort or speed to choose; `codaService` and
|
|
34
|
+
// `codaGroup` say whose they are, for the session's move and the model's group.
|
|
35
|
+
export async function serviceModels({ ref, title, url, key }, fetcher = fetch, state) {
|
|
36
|
+
const listed = await lastListed(ref, async () => {
|
|
37
|
+
const at = `${url.replace(/\/+$/, '')}/models`;
|
|
38
|
+
const response = await fetcher(at, { headers: key ? { Authorization: `Bearer ${key}` } : {}, signal: AbortSignal.timeout(LIST_TIMEOUT) });
|
|
39
|
+
if (!response.ok) throw new Error(`${at} 回答 ${response.status}`);
|
|
40
|
+
const body = await response.json();
|
|
41
|
+
// Only a list replaces the one kept: an answer of another shape is the service not answering.
|
|
42
|
+
if (!Array.isArray(body?.data)) throw new Error(`${at} 的回答不是模型列表`);
|
|
43
|
+
return body.data.filter(model => typeof model?.id === 'string' && model.id);
|
|
44
|
+
}, state, log);
|
|
45
|
+
return listed.map(model => ({
|
|
40
46
|
id: `${ref}/${model.id}`, model: model.id, displayName: model.id, description: '', hidden: false, isDefault: false,
|
|
41
47
|
supportedReasoningEfforts: [], defaultReasoningEffort: 'medium', inputModalities: ['text', 'image'], serviceTiers: [],
|
|
42
48
|
codaService: ref, codaGroup: title }));
|
|
@@ -47,10 +53,11 @@ export class ServiceModels {
|
|
|
47
53
|
// `on`: the service the session runs on, null on Codex's own (where it opens), undefined when a move to one or another
|
|
48
54
|
// failed partway: codex-acp sets its gateway before the thread is resumed on it, so that is no sign of either, and keeps
|
|
49
55
|
// its old client, closed, when the app-server it started again does not start.
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
// `state`: the plugin's state directory, where what each service last listed is kept.
|
|
57
|
+
constructor(services = chosenServices(), fetcher = fetch, state = null) { Object.assign(this, { services, fetcher, state }); this.models = []; this.on = null; }
|
|
58
|
+
// Theirs in the order the services are chosen: none of one that cannot list them now and never did.
|
|
52
59
|
list() {
|
|
53
|
-
return this.listed ??= Promise.all(this.services.map(service => serviceModels(service, this.fetcher)
|
|
60
|
+
return this.listed ??= Promise.all(this.services.map(service => serviceModels(service, this.fetcher, this.state)
|
|
54
61
|
.catch(e => { log(`模型服务 ${service.ref} 列不出模型:${e.message}`); return []; }))).then(lists => (this.models = lists.flat()));
|
|
55
62
|
}
|
|
56
63
|
served(model) { return this.models.find(m => m.id === model) ?? null; }
|
|
@@ -106,7 +113,7 @@ export class ServiceModels {
|
|
|
106
113
|
}
|
|
107
114
|
const firstWords = prompt => Array.from((prompt ?? []).filter(b => b.type === 'text').map(b => b.text).join(' ').replace(/\s+/g, ' ').trim()).slice(0, 80).join('') || 'Coda';
|
|
108
115
|
|
|
109
|
-
export const services = new ServiceModels();
|
|
116
|
+
export const services = new ServiceModels(chosenServices(), fetch, process.env.CODA_PLUGIN_STATE);
|
|
110
117
|
const patches = [
|
|
111
118
|
// Codex's models, then the services'.
|
|
112
119
|
[' } while (cursor);\n return models;\n }', ' } while (cursor);\n return [...models, ...await codaServices.list()];\n }', 1],
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "codex",
|
|
4
4
|
"displayName": "Codex",
|
|
5
5
|
"description": "Codex Agent:Coda 自带的入口运行 codex-acp,报告子 Agent 与后台命令。额度与登录由账号来源插件接管;未接入账号来源时使用 Codex 自己的登录面板。",
|
|
6
|
-
"version": "1.8.
|
|
6
|
+
"version": "1.8.2",
|
|
7
7
|
"categories": ["Agent"],
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@agentclientprotocol/codex-acp": "1.12.0",
|