@coda-console/codex 1.7.1 → 1.8.0
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/providers.mjs +55 -34
- package/package.json +9 -5
package/agent/providers.mjs
CHANGED
|
@@ -1,54 +1,72 @@
|
|
|
1
|
-
// The model
|
|
2
|
-
//
|
|
3
|
-
// lists (`<地址>/models`) join Codex's own,
|
|
4
|
-
// provider, so a turn on another provider's model first moves the session there the way
|
|
5
|
-
// `providers/set` and `providers/disable`: its app-server starts again with
|
|
6
|
-
// (or without
|
|
1
|
+
// The model services chosen in Coda (`codex.modelProviders`, Coda's docs/plugins.md「模型服务」): other plugins' services
|
|
2
|
+
// that speak the OpenAI Responses API (Codex no longer speaks Chat Completions), from CODA_MODEL_PROVIDERS. The models each
|
|
3
|
+
// lists (`<地址>/models`) join Codex's own, a group of the service's each (Coda's docs/agent-protocol.md「模型的分组」). Codex
|
|
4
|
+
// binds a thread to one model provider, so a turn on another provider's model first moves the session there the way
|
|
5
|
+
// codex-acp moves it for ACP's `providers/set` and `providers/disable`: its app-server starts again with that service as the
|
|
6
|
+
// provider of its OpenAI slot (or without one), and the session's thread is resumed on it. The machine's Codex
|
|
7
|
+
// configuration is not touched.
|
|
7
8
|
import { existsSync, readFileSync } from 'node:fs';
|
|
8
9
|
import { carriedInstructions } from './instructions.mjs';
|
|
9
10
|
|
|
10
|
-
export const SETTING = 'codex.
|
|
11
|
+
export const SETTING = 'codex.modelProviders';
|
|
11
12
|
const log = text => process.stderr.write(`[codex-agent] ${text}\n`);
|
|
12
13
|
const parse = text => { try { return JSON.parse(text || '{}'); } catch { return {}; } };
|
|
13
14
|
const LIST_TIMEOUT = 8000;
|
|
14
15
|
|
|
15
|
-
// The chosen
|
|
16
|
-
//
|
|
17
|
-
export function
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
// The services chosen, in the order chosen: each its address, title and key (read from the file Coda keeps it in). One not
|
|
17
|
+
// on this machine, or speaking another API, is left out.
|
|
18
|
+
export function chosenServices(env = process.env) {
|
|
19
|
+
const refs = parse(env.CODA_PLUGIN_SETTINGS)[SETTING], provided = parse(env.CODA_MODEL_PROVIDERS);
|
|
20
|
+
return (Array.isArray(refs) ? refs : []).flatMap(ref => {
|
|
21
|
+
const service = provided[ref];
|
|
22
|
+
if (!service?.url) { log(`模型服务 ${ref} 在这台机器上没有`); return []; }
|
|
23
|
+
if (service.api !== 'openai-responses') { log(`模型服务 ${ref} 讲 ${service.api},Codex 只接 openai-responses 的`); return []; }
|
|
24
|
+
let key;
|
|
25
|
+
try { key = service.keyFile ? readFileSync(service.keyFile, 'utf8').trim() || undefined : undefined; }
|
|
26
|
+
catch (e) { log(`读不到模型服务 ${ref} 的 Key:${e.message}`); }
|
|
27
|
+
return [{ ref, title: service.title || ref, url: service.url, key }];
|
|
28
|
+
});
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
// The models
|
|
30
|
-
// model
|
|
31
|
+
// The models a service lists, as Codex's model list has them (app-server `model/list`): `<服务>/<模型>`, named by the
|
|
32
|
+
// model, with no effort or speed to choose; `codaService` and `codaGroup` say whose they are, for the session's move and
|
|
33
|
+
// the model's group.
|
|
31
34
|
export async function serviceModels({ ref, title, url, key }, fetcher = fetch) {
|
|
32
35
|
const at = `${url.replace(/\/+$/, '')}/models`;
|
|
33
36
|
const response = await fetcher(at, { headers: key ? { Authorization: `Bearer ${key}` } : {}, signal: AbortSignal.timeout(LIST_TIMEOUT) });
|
|
34
37
|
if (!response.ok) throw new Error(`${at} 回答 ${response.status}`);
|
|
35
38
|
const body = await response.json();
|
|
36
39
|
return (Array.isArray(body?.data) ? body.data : []).filter(model => typeof model?.id === 'string' && model.id).map(model => ({
|
|
37
|
-
id: `${ref}/${model.id}`, model: model.id, displayName:
|
|
38
|
-
supportedReasoningEfforts: [], defaultReasoningEffort: 'medium', inputModalities: ['text', 'image'], serviceTiers: []
|
|
40
|
+
id: `${ref}/${model.id}`, model: model.id, displayName: model.id, description: '', hidden: false, isDefault: false,
|
|
41
|
+
supportedReasoningEfforts: [], defaultReasoningEffort: 'medium', inputModalities: ['text', 'image'], serviceTiers: [],
|
|
42
|
+
codaService: ref, codaGroup: title }));
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
//
|
|
45
|
+
// The services a process has (Coda runs one session a process), listed once, as its session opens.
|
|
42
46
|
export class ServiceModels {
|
|
43
|
-
// `on`: the service
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
constructor(
|
|
47
|
+
// `on`: the service the session runs on, null on Codex's own (where it opens), undefined when a move to one or another
|
|
48
|
+
// failed partway: codex-acp sets its gateway before the thread is resumed on it, so that is no sign of either, and keeps
|
|
49
|
+
// its old client, closed, when the app-server it started again does not start.
|
|
50
|
+
constructor(services = chosenServices(), fetcher = fetch) { Object.assign(this, { services, fetcher }); this.models = []; this.on = null; }
|
|
51
|
+
// Theirs in the order the services are chosen: none of one that cannot list them now.
|
|
47
52
|
list() {
|
|
48
|
-
return this.listed ??= (
|
|
49
|
-
.catch(e => { log(`模型服务 ${
|
|
53
|
+
return this.listed ??= Promise.all(this.services.map(service => serviceModels(service, this.fetcher)
|
|
54
|
+
.catch(e => { log(`模型服务 ${service.ref} 列不出模型:${e.message}`); return []; }))).then(lists => (this.models = lists.flat()));
|
|
50
55
|
}
|
|
51
56
|
served(model) { return this.models.find(m => m.id === model) ?? null; }
|
|
57
|
+
// The model select's options (codex-acp's, of Codex's models and the services'), grouped by where they come from once a
|
|
58
|
+
// service's are among them: Codex's own, then each service's under its title. Codex's alone are one plain list.
|
|
59
|
+
grouped(options) {
|
|
60
|
+
if (!options.some(option => this.served(option.value))) return options;
|
|
61
|
+
const groups = [{ group: 'codex', name: 'Codex', options: [] }];
|
|
62
|
+
for (const option of options) {
|
|
63
|
+
const served = this.served(option.value), id = served?.codaService ?? 'codex';
|
|
64
|
+
let group = groups.find(g => g.group === id);
|
|
65
|
+
if (!group) groups.push(group = { group: id, name: served.codaGroup, options: [] });
|
|
66
|
+
group.options.push(option);
|
|
67
|
+
}
|
|
68
|
+
return groups.filter(g => g.options.length);
|
|
69
|
+
}
|
|
52
70
|
// What a turn names to Codex: the service's own name for one of its models.
|
|
53
71
|
wire(model) { return this.served(model)?.model ?? model; }
|
|
54
72
|
// A stop that comes while a turn waits on the move has no turn of codex-acp's to interrupt yet: it is the waiting turn's,
|
|
@@ -66,7 +84,8 @@ export class ServiceModels {
|
|
|
66
84
|
async move(server, { sessionId, prompt }) {
|
|
67
85
|
const state = server.sessions.get(sessionId);
|
|
68
86
|
if (!state) return;
|
|
69
|
-
const served = this.served(state.currentModelId.replace(/\[[^\]]*\]$/, ''))
|
|
87
|
+
const served = this.served(state.currentModelId.replace(/\[[^\]]*\]$/, ''));
|
|
88
|
+
const service = served ? this.services.find(s => s.ref === served.codaService) : null, wanted = service?.ref ?? null;
|
|
70
89
|
if (this.on === wanted) return;
|
|
71
90
|
if (this.on !== undefined) {
|
|
72
91
|
const client = server.codexAcpClient.appServerClient;
|
|
@@ -74,8 +93,8 @@ export class ServiceModels {
|
|
|
74
93
|
if (thread.path && !existsSync(thread.path)) await client.threadSetName({ threadId: sessionId, name: firstWords(prompt) });
|
|
75
94
|
}
|
|
76
95
|
this.on = undefined;
|
|
77
|
-
await carriedInstructions(() =>
|
|
78
|
-
headers:
|
|
96
|
+
await carriedInstructions(() => service ? server.setProvider({ providerId: 'openai', apiType: 'openai', baseUrl: service.url,
|
|
97
|
+
headers: service.key ? { Authorization: `Bearer ${service.key}` } : {} }) : server.disableProvider({ providerId: 'openai' }));
|
|
79
98
|
this.on = wanted;
|
|
80
99
|
}
|
|
81
100
|
// Codex knows nothing of the service's models: that it runs one on fallback metadata, or resumes a session last on one
|
|
@@ -89,8 +108,10 @@ const firstWords = prompt => Array.from((prompt ?? []).filter(b => b.type === 't
|
|
|
89
108
|
|
|
90
109
|
export const services = new ServiceModels();
|
|
91
110
|
const patches = [
|
|
92
|
-
// Codex's models, then the
|
|
111
|
+
// Codex's models, then the services'.
|
|
93
112
|
[' } while (cursor);\n return models;\n }', ' } while (cursor);\n return [...models, ...await codaServices.list()];\n }', 1],
|
|
113
|
+
// Listed in groups once a service's are among them.
|
|
114
|
+
[' currentValue: currentBaseModelId,\n options,\n', ' currentValue: currentBaseModelId,\n options: codaServices.grouped(options),\n', 1],
|
|
94
115
|
// A turn first moves the session to the provider of its model, and names the model as that provider does.
|
|
95
116
|
[' async prompt(params, signal, onTurnStarted) {\n if (this.providerUpdate !== null) {',
|
|
96
117
|
' async prompt(params, signal, onTurnStarted) {\n if (await codaServices.route(this, params)) return { stopReason: "cancelled" };\n if (this.providerUpdate !== null) {', 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.
|
|
6
|
+
"version": "1.8.0",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@agentclientprotocol/codex-acp": "1.12.0",
|
|
9
9
|
"@openai/codex": "0.162.1"
|
|
@@ -40,12 +40,16 @@
|
|
|
40
40
|
"configuration": {
|
|
41
41
|
"title": "Codex",
|
|
42
42
|
"properties": {
|
|
43
|
-
"codex.
|
|
44
|
-
"type": "
|
|
45
|
-
"
|
|
43
|
+
"codex.modelProviders": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"items": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"format": "coda-model-provider",
|
|
48
|
+
"modelApis": ["openai-responses"]
|
|
49
|
+
},
|
|
46
50
|
"title": "模型服务",
|
|
47
51
|
"order": 1,
|
|
48
|
-
"description": "
|
|
52
|
+
"description": "勾选插件提供的模型服务(如 Magpie),每个服务各成一组,它列出的模型和 Codex 自己的模型一起出现在会话的模型里;选中哪个服务的模型,那个会话就走那个服务,Codex 自己的模型照旧。只列 OpenAI Responses 接口(openai-responses)的服务:Codex 不再支持 Chat Completions。"
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
},
|