@gengjiawen/os-init 1.18.1 → 1.20.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/.github/workflows/nodejs.yml +6 -3
- package/CHANGELOG.md +26 -0
- package/bin/bin.js +5 -2
- package/build/all-agents.d.ts +2 -1
- package/build/all-agents.js +2 -2
- package/build/claude-code.js +1 -0
- package/build/codex.d.ts +3 -2
- package/build/codex.js +43 -6
- package/build/opencode.js +25 -13
- package/libs/all-agents.test.ts +16 -4
- package/libs/all-agents.ts +4 -4
- package/libs/claude-code.test.ts +7 -0
- package/libs/claude-code.ts +1 -0
- package/libs/codex.test.ts +114 -6
- package/libs/codex.ts +61 -8
- package/libs/opencode.test.ts +27 -9
- package/libs/opencode.ts +29 -13
- package/package.json +1 -1
|
@@ -21,11 +21,14 @@ jobs:
|
|
|
21
21
|
version: 9
|
|
22
22
|
- name: Envinfo
|
|
23
23
|
run: npx envinfo
|
|
24
|
-
- name: Install
|
|
24
|
+
- name: Install and build (pnpm)
|
|
25
25
|
run: |
|
|
26
26
|
pnpm install --frozen-lockfile=false
|
|
27
27
|
pnpm build
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
- name: Check formatting (pnpm)
|
|
29
|
+
if: runner.os != 'Windows'
|
|
30
|
+
run: pnpm format:check
|
|
31
|
+
- name: Test (pnpm)
|
|
32
|
+
run: pnpm test
|
|
30
33
|
env:
|
|
31
34
|
CI: true
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.20.0](https://github.com/gengjiawen/os-init/compare/v1.19.0...v1.20.0) (2026-03-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add more opencode models ([e04e0d1](https://github.com/gengjiawen/os-init/commit/e04e0d1fc52b7a93cf51436d23c82b1e725daff6))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* add Claude nonessential traffic env ([9bf1bac](https://github.com/gengjiawen/os-init/commit/9bf1bac9f09e8c9a3e8c740834a1d5ad7986f1b9))
|
|
14
|
+
* CI ([166259f](https://github.com/gengjiawen/os-init/commit/166259f104bdda2dc57cd4678d9e03d739c472a9))
|
|
15
|
+
* use absolute codex model catalog path on windows ([fb8f19f](https://github.com/gengjiawen/os-init/commit/fb8f19f8f6b3de437b2192e57ec7e7fe1e48ee5a))
|
|
16
|
+
|
|
17
|
+
## [1.19.0](https://github.com/gengjiawen/os-init/compare/v1.18.1...v1.19.0) (2026-03-19)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* refresh codex model catalog during setup ([9ed724a](https://github.com/gengjiawen/os-init/commit/9ed724accda9901a813bb9265895db6879d406bb))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* add model metadata to opencode config ([59ade2c](https://github.com/gengjiawen/os-init/commit/59ade2c9e9daa307de56d30968a1c3e8977ea945))
|
|
28
|
+
|
|
3
29
|
## [1.18.1](https://github.com/gengjiawen/os-init/compare/v1.18.0...v1.18.1) (2026-03-16)
|
|
4
30
|
|
|
5
31
|
|
package/bin/bin.js
CHANGED
|
@@ -65,9 +65,11 @@ program
|
|
|
65
65
|
return
|
|
66
66
|
}
|
|
67
67
|
try {
|
|
68
|
-
const { configPath, authPath } =
|
|
68
|
+
const { configPath, authPath, catalogPath } =
|
|
69
|
+
await writeCodexConfig(apiKey)
|
|
69
70
|
console.log(`Codex config written to: ${configPath}`)
|
|
70
71
|
console.log(`Codex auth written to: ${authPath}`)
|
|
72
|
+
console.log(`Codex model catalog written to: ${catalogPath}`)
|
|
71
73
|
await installCodexDeps()
|
|
72
74
|
} catch (err) {
|
|
73
75
|
console.error('Failed to setup Codex:', err.message)
|
|
@@ -146,7 +148,7 @@ program
|
|
|
146
148
|
: 'Setting up Claude Code + Codex + OpenCode...\n'
|
|
147
149
|
)
|
|
148
150
|
|
|
149
|
-
const result = writeAllAgentsConfig(apiKey, { full })
|
|
151
|
+
const result = await writeAllAgentsConfig(apiKey, { full })
|
|
150
152
|
|
|
151
153
|
console.log('Claude Code:')
|
|
152
154
|
console.log(` Settings written to: ${result.claude.settingsPath}`)
|
|
@@ -157,6 +159,7 @@ program
|
|
|
157
159
|
console.log('\nCodex:')
|
|
158
160
|
console.log(` Config written to: ${result.codex.configPath}`)
|
|
159
161
|
console.log(` Auth written to: ${result.codex.authPath}`)
|
|
162
|
+
console.log(` Model catalog written to: ${result.codex.catalogPath}`)
|
|
160
163
|
|
|
161
164
|
console.log('\nOpenCode:')
|
|
162
165
|
console.log(` Config written to: ${result.opencode.configPath}`)
|
package/build/all-agents.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface AllAgentsResult {
|
|
|
9
9
|
codex: {
|
|
10
10
|
configPath: string;
|
|
11
11
|
authPath: string;
|
|
12
|
+
catalogPath: string;
|
|
12
13
|
};
|
|
13
14
|
opencode: {
|
|
14
15
|
configPath: string;
|
|
@@ -18,5 +19,5 @@ export interface AllAgentsResult {
|
|
|
18
19
|
settingsPath: string;
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
|
-
export declare function writeAllAgentsConfig(apiKey: string, options?: AllAgentsOptions): AllAgentsResult
|
|
22
|
+
export declare function writeAllAgentsConfig(apiKey: string, options?: AllAgentsOptions): Promise<AllAgentsResult>;
|
|
22
23
|
export declare function installAllAgentsDeps(options?: AllAgentsOptions): Promise<void>;
|
package/build/all-agents.js
CHANGED
|
@@ -6,9 +6,9 @@ const claude_code_1 = require("./claude-code");
|
|
|
6
6
|
const codex_1 = require("./codex");
|
|
7
7
|
const gemini_cli_1 = require("./gemini-cli");
|
|
8
8
|
const opencode_1 = require("./opencode");
|
|
9
|
-
function writeAllAgentsConfig(apiKey, options = {}) {
|
|
9
|
+
async function writeAllAgentsConfig(apiKey, options = {}) {
|
|
10
10
|
const claudeResult = (0, claude_code_1.writeClaudeConfig)(apiKey);
|
|
11
|
-
const codexResult = (0, codex_1.writeCodexConfig)(apiKey);
|
|
11
|
+
const codexResult = await (0, codex_1.writeCodexConfig)(apiKey);
|
|
12
12
|
const opencodeResult = (0, opencode_1.writeOpencodeConfig)(apiKey);
|
|
13
13
|
const result = {
|
|
14
14
|
claude: claudeResult,
|
package/build/claude-code.js
CHANGED
|
@@ -62,6 +62,7 @@ function writeVSCodeClaudePluginConfig(apiKey) {
|
|
|
62
62
|
const edits = (0, jsonc_parser_1.modify)(sourceContent, ['claudeCode.environmentVariables'], [
|
|
63
63
|
{ name: 'ANTHROPIC_BASE_URL', value: CLAUDE_BASE_URL },
|
|
64
64
|
{ name: 'ANTHROPIC_AUTH_TOKEN', value: apiKey },
|
|
65
|
+
{ name: 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', value: '1' },
|
|
65
66
|
], {
|
|
66
67
|
formattingOptions: {
|
|
67
68
|
insertSpaces: true,
|
package/build/codex.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export declare function writeCodexConfig(apiKey: string): {
|
|
1
|
+
export declare function writeCodexConfig(apiKey: string): Promise<{
|
|
2
2
|
configPath: string;
|
|
3
3
|
authPath: string;
|
|
4
|
-
|
|
4
|
+
catalogPath: string;
|
|
5
|
+
}>;
|
|
5
6
|
export declare function installCodexDeps(): Promise<void>;
|
package/build/codex.js
CHANGED
|
@@ -8,10 +8,22 @@ const os = require("os");
|
|
|
8
8
|
const TOML = require("@iarna/toml");
|
|
9
9
|
const execa_1 = require("execa");
|
|
10
10
|
const utils_1 = require("./utils");
|
|
11
|
+
const CODEX_BASE_URL = 'https://ai.gengjiawen.com/api/openai';
|
|
12
|
+
const CODEX_MODEL_CATALOG_CONFIG_PATH = '~/.codex/remote-model-catalog.json';
|
|
13
|
+
const CODEX_MODEL_CATALOG_FILENAME = 'remote-model-catalog.json';
|
|
11
14
|
function getCodexConfigDir() {
|
|
12
15
|
return path.join(os.homedir(), '.codex');
|
|
13
16
|
}
|
|
14
|
-
|
|
17
|
+
function getCodexModelCatalogPath() {
|
|
18
|
+
return path.join(getCodexConfigDir(), CODEX_MODEL_CATALOG_FILENAME);
|
|
19
|
+
}
|
|
20
|
+
function getCodexModelCatalogConfigPath() {
|
|
21
|
+
return os.platform() === 'win32'
|
|
22
|
+
? getCodexModelCatalogPath()
|
|
23
|
+
: CODEX_MODEL_CATALOG_CONFIG_PATH;
|
|
24
|
+
}
|
|
25
|
+
function getCodexConfigTomlTemplate() {
|
|
26
|
+
return `model_provider = "jw"
|
|
15
27
|
model = "gpt-5.4"
|
|
16
28
|
model_reasoning_effort = "high"
|
|
17
29
|
plan_mode_reasoning_effort = "xhigh"
|
|
@@ -19,12 +31,14 @@ model_auto_compact_token_limit = 131072
|
|
|
19
31
|
disable_response_storage = true
|
|
20
32
|
preferred_auth_method = "apikey"
|
|
21
33
|
service_tier = "fast"
|
|
34
|
+
model_catalog_json = ${JSON.stringify(getCodexModelCatalogConfigPath())}
|
|
22
35
|
|
|
23
36
|
[model_providers.jw]
|
|
24
37
|
name = "jw"
|
|
25
|
-
base_url = "
|
|
38
|
+
base_url = "${CODEX_BASE_URL}"
|
|
26
39
|
wire_api = "responses"
|
|
27
40
|
`;
|
|
41
|
+
}
|
|
28
42
|
function isTomlTable(value) {
|
|
29
43
|
return (value !== undefined &&
|
|
30
44
|
typeof value === 'object' &&
|
|
@@ -44,21 +58,44 @@ function mergeTomlTables(existingConfig, templateConfig) {
|
|
|
44
58
|
}
|
|
45
59
|
function getMergedCodexConfig(existingContent) {
|
|
46
60
|
const existingConfig = TOML.parse(existingContent);
|
|
47
|
-
const templateConfig = TOML.parse(
|
|
61
|
+
const templateConfig = TOML.parse(getCodexConfigTomlTemplate());
|
|
48
62
|
return TOML.stringify(mergeTomlTables(existingConfig, templateConfig));
|
|
49
63
|
}
|
|
50
|
-
function
|
|
64
|
+
async function refreshCodexModelCatalog(apiKey) {
|
|
65
|
+
if (typeof fetch !== 'function') {
|
|
66
|
+
throw new Error('Global fetch API is unavailable in this Node.js runtime.');
|
|
67
|
+
}
|
|
68
|
+
const response = await fetch(`${CODEX_BASE_URL}/models`, {
|
|
69
|
+
headers: {
|
|
70
|
+
Accept: 'application/json',
|
|
71
|
+
Authorization: `Bearer ${apiKey}`,
|
|
72
|
+
'User-Agent': '@gengjiawen/os-init',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`Failed to refresh Codex model catalog: ${response.status} ${response.statusText}`);
|
|
77
|
+
}
|
|
78
|
+
const catalog = (await response.json());
|
|
79
|
+
if (!Array.isArray(catalog?.models) || catalog.models.length === 0) {
|
|
80
|
+
throw new Error('Failed to refresh Codex model catalog: response does not contain any models.');
|
|
81
|
+
}
|
|
82
|
+
const catalogPath = getCodexModelCatalogPath();
|
|
83
|
+
fs.writeFileSync(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
|
|
84
|
+
return catalogPath;
|
|
85
|
+
}
|
|
86
|
+
async function writeCodexConfig(apiKey) {
|
|
51
87
|
const configDir = getCodexConfigDir();
|
|
52
88
|
(0, utils_1.ensureDir)(configDir);
|
|
89
|
+
const catalogPath = await refreshCodexModelCatalog(apiKey);
|
|
53
90
|
const configPath = path.join(configDir, 'config.toml');
|
|
54
91
|
const configContent = fs.existsSync(configPath)
|
|
55
92
|
? getMergedCodexConfig(fs.readFileSync(configPath, 'utf8'))
|
|
56
|
-
:
|
|
93
|
+
: getCodexConfigTomlTemplate();
|
|
57
94
|
fs.writeFileSync(configPath, configContent);
|
|
58
95
|
const authPath = path.join(configDir, 'auth.json');
|
|
59
96
|
const authContent = JSON.stringify({ OPENAI_API_KEY: apiKey }, null, 2);
|
|
60
97
|
fs.writeFileSync(authPath, authContent);
|
|
61
|
-
return { configPath, authPath };
|
|
98
|
+
return { configPath, authPath, catalogPath };
|
|
62
99
|
}
|
|
63
100
|
async function installCodexDeps() {
|
|
64
101
|
const packages = ['@openai/codex'];
|
package/build/opencode.js
CHANGED
|
@@ -9,9 +9,31 @@ const execa_1 = require("execa");
|
|
|
9
9
|
const utils_1 = require("./utils");
|
|
10
10
|
const OPENCODE_PROVIDER_ID = 'MyCustomProvider';
|
|
11
11
|
const OPENCODE_MODEL_ID = 'code';
|
|
12
|
-
const
|
|
13
|
-
|
|
12
|
+
const OPENCODE_MODEL_IDS = [
|
|
13
|
+
OPENCODE_MODEL_ID,
|
|
14
|
+
'glm',
|
|
15
|
+
'kimi',
|
|
16
|
+
'minimax',
|
|
17
|
+
'deepseek',
|
|
18
|
+
'gemini-flash',
|
|
19
|
+
];
|
|
14
20
|
const OPENCODE_BASE_URL = 'https://ai.gengjiawen.com/api/openai/v1';
|
|
21
|
+
function createOpencodeModelConfig(modelId) {
|
|
22
|
+
return {
|
|
23
|
+
name: modelId,
|
|
24
|
+
attachment: true,
|
|
25
|
+
modalities: {
|
|
26
|
+
input: ['text', 'image'],
|
|
27
|
+
output: ['text'],
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function createOpencodeModelsConfig() {
|
|
32
|
+
return Object.fromEntries(OPENCODE_MODEL_IDS.map((modelId) => [
|
|
33
|
+
modelId,
|
|
34
|
+
createOpencodeModelConfig(modelId),
|
|
35
|
+
]));
|
|
36
|
+
}
|
|
15
37
|
function getOpencodeConfigDir() {
|
|
16
38
|
return path.join(os.homedir(), '.config', 'opencode');
|
|
17
39
|
}
|
|
@@ -28,17 +50,7 @@ function writeOpencodeConfig(apiKey) {
|
|
|
28
50
|
baseURL: OPENCODE_BASE_URL,
|
|
29
51
|
apiKey,
|
|
30
52
|
},
|
|
31
|
-
models:
|
|
32
|
-
[OPENCODE_MODEL_ID]: {
|
|
33
|
-
name: OPENCODE_MODEL_ID,
|
|
34
|
-
},
|
|
35
|
-
[OPENCODE_GLM_MODEL_ID]: {
|
|
36
|
-
name: OPENCODE_GLM_MODEL_ID,
|
|
37
|
-
},
|
|
38
|
-
[OPENCODE_KIMI_MODEL_ID]: {
|
|
39
|
-
name: OPENCODE_KIMI_MODEL_ID,
|
|
40
|
-
},
|
|
41
|
-
},
|
|
53
|
+
models: createOpencodeModelsConfig(),
|
|
42
54
|
},
|
|
43
55
|
},
|
|
44
56
|
model: `${OPENCODE_PROVIDER_ID}/${OPENCODE_MODEL_ID}`,
|
package/libs/all-agents.test.ts
CHANGED
|
@@ -12,12 +12,18 @@ describe('writeAllAgentsConfig', () => {
|
|
|
12
12
|
let tempHome: string
|
|
13
13
|
let homedirSpy: jest.SpiedFunction<typeof os.homedir>
|
|
14
14
|
let originalAppData: string | undefined
|
|
15
|
+
let originalFetch: typeof global.fetch | undefined
|
|
15
16
|
|
|
16
17
|
beforeEach(() => {
|
|
17
18
|
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'os-init-all-agents-'))
|
|
18
19
|
homedirSpy = jest.spyOn(os, 'homedir').mockReturnValue(tempHome)
|
|
19
20
|
originalAppData = process.env.APPDATA
|
|
21
|
+
originalFetch = global.fetch
|
|
20
22
|
process.env.APPDATA = path.join(tempHome, 'AppData', 'Roaming')
|
|
23
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
24
|
+
ok: true,
|
|
25
|
+
json: async () => ({ models: [{ id: 'gpt-5.4' }] }),
|
|
26
|
+
}) as unknown as typeof global.fetch
|
|
21
27
|
})
|
|
22
28
|
|
|
23
29
|
afterEach(() => {
|
|
@@ -27,21 +33,27 @@ describe('writeAllAgentsConfig', () => {
|
|
|
27
33
|
} else {
|
|
28
34
|
process.env.APPDATA = originalAppData
|
|
29
35
|
}
|
|
36
|
+
if (originalFetch === undefined) {
|
|
37
|
+
global.fetch = undefined as unknown as typeof global.fetch
|
|
38
|
+
} else {
|
|
39
|
+
global.fetch = originalFetch
|
|
40
|
+
}
|
|
30
41
|
fs.rmSync(tempHome, { recursive: true, force: true })
|
|
31
42
|
})
|
|
32
43
|
|
|
33
|
-
test('writes Claude, Codex and OpenCode config by default', () => {
|
|
34
|
-
const result = writeAllAgentsConfig('test-api-key')
|
|
44
|
+
test('writes Claude, Codex and OpenCode config by default', async () => {
|
|
45
|
+
const result = await writeAllAgentsConfig('test-api-key')
|
|
35
46
|
|
|
36
47
|
expect(fs.existsSync(result.claude.settingsPath)).toBe(true)
|
|
37
48
|
expect(fs.existsSync(result.codex.configPath)).toBe(true)
|
|
38
49
|
expect(fs.existsSync(result.codex.authPath)).toBe(true)
|
|
50
|
+
expect(fs.existsSync(result.codex.catalogPath)).toBe(true)
|
|
39
51
|
expect(fs.existsSync(result.opencode.configPath)).toBe(true)
|
|
40
52
|
expect(result.gemini).toBeUndefined()
|
|
41
53
|
})
|
|
42
54
|
|
|
43
|
-
test('includes Gemini config when full option is enabled', () => {
|
|
44
|
-
const result = writeAllAgentsConfig('test-api-key', { full: true })
|
|
55
|
+
test('includes Gemini config when full option is enabled', async () => {
|
|
56
|
+
const result = await writeAllAgentsConfig('test-api-key', { full: true })
|
|
45
57
|
|
|
46
58
|
expect(result.gemini).toBeDefined()
|
|
47
59
|
expect(fs.existsSync(result.gemini!.envPath)).toBe(true)
|
package/libs/all-agents.ts
CHANGED
|
@@ -10,18 +10,18 @@ export interface AllAgentsOptions {
|
|
|
10
10
|
|
|
11
11
|
export interface AllAgentsResult {
|
|
12
12
|
claude: { settingsPath: string; vscodeSettingsPath: string }
|
|
13
|
-
codex: { configPath: string; authPath: string }
|
|
13
|
+
codex: { configPath: string; authPath: string; catalogPath: string }
|
|
14
14
|
opencode: { configPath: string }
|
|
15
15
|
gemini?: { envPath: string; settingsPath: string }
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Write configuration for the combined setup (Claude Code + Codex + OpenCode, optional Gemini CLI). */
|
|
19
|
-
export function writeAllAgentsConfig(
|
|
19
|
+
export async function writeAllAgentsConfig(
|
|
20
20
|
apiKey: string,
|
|
21
21
|
options: AllAgentsOptions = {}
|
|
22
|
-
): AllAgentsResult {
|
|
22
|
+
): Promise<AllAgentsResult> {
|
|
23
23
|
const claudeResult = writeClaudeConfig(apiKey)
|
|
24
|
-
const codexResult = writeCodexConfig(apiKey)
|
|
24
|
+
const codexResult = await writeCodexConfig(apiKey)
|
|
25
25
|
const opencodeResult = writeOpencodeConfig(apiKey)
|
|
26
26
|
|
|
27
27
|
const result: AllAgentsResult = {
|
package/libs/claude-code.test.ts
CHANGED
|
@@ -71,12 +71,19 @@ describe('writeClaudeConfig', () => {
|
|
|
71
71
|
expect(claudeSettings).toContain(
|
|
72
72
|
'"ANTHROPIC_BASE_URL": "https://ai.gengjiawen.com/api/claude/"'
|
|
73
73
|
)
|
|
74
|
+
expect(claudeSettings).toContain(
|
|
75
|
+
'"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"'
|
|
76
|
+
)
|
|
74
77
|
|
|
75
78
|
const vscodeSettings = fs.readFileSync(result.vscodeSettingsPath, 'utf8')
|
|
76
79
|
expect(vscodeSettings).toContain('"editor.fontSize": 14')
|
|
77
80
|
expect(vscodeSettings).toContain('"claudeCode.environmentVariables"')
|
|
78
81
|
expect(vscodeSettings).toContain('"ANTHROPIC_BASE_URL"')
|
|
79
82
|
expect(vscodeSettings).toContain('"ANTHROPIC_AUTH_TOKEN"')
|
|
83
|
+
expect(vscodeSettings).toContain(
|
|
84
|
+
'"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"'
|
|
85
|
+
)
|
|
86
|
+
expect(vscodeSettings).toContain('"1"')
|
|
80
87
|
expect(vscodeSettings).toContain('"test-api-key"')
|
|
81
88
|
})
|
|
82
89
|
|
package/libs/claude-code.ts
CHANGED
|
@@ -94,6 +94,7 @@ function writeVSCodeClaudePluginConfig(apiKey: string): {
|
|
|
94
94
|
[
|
|
95
95
|
{ name: 'ANTHROPIC_BASE_URL', value: CLAUDE_BASE_URL },
|
|
96
96
|
{ name: 'ANTHROPIC_AUTH_TOKEN', value: apiKey },
|
|
97
|
+
{ name: 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', value: '1' },
|
|
97
98
|
],
|
|
98
99
|
{
|
|
99
100
|
formattingOptions: {
|
package/libs/codex.test.ts
CHANGED
|
@@ -12,27 +12,57 @@ import { writeCodexConfig } from './codex'
|
|
|
12
12
|
describe('writeCodexConfig', () => {
|
|
13
13
|
let tempHome: string
|
|
14
14
|
let homedirSpy: jest.SpiedFunction<typeof os.homedir>
|
|
15
|
+
let originalFetch: typeof global.fetch | undefined
|
|
16
|
+
|
|
17
|
+
function getExpectedModelCatalogConfigPath(): string {
|
|
18
|
+
return os.platform() === 'win32'
|
|
19
|
+
? path.join(tempHome, '.codex', 'remote-model-catalog.json')
|
|
20
|
+
: '~/.codex/remote-model-catalog.json'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mockCatalogFetch(
|
|
24
|
+
catalog: { models: Array<Record<string, unknown>> } = {
|
|
25
|
+
models: [{ id: 'gpt-5.4' }],
|
|
26
|
+
}
|
|
27
|
+
): jest.Mock {
|
|
28
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
29
|
+
ok: true,
|
|
30
|
+
json: async () => catalog,
|
|
31
|
+
})
|
|
32
|
+
global.fetch = fetchMock as unknown as typeof global.fetch
|
|
33
|
+
return fetchMock
|
|
34
|
+
}
|
|
15
35
|
|
|
16
36
|
beforeEach(() => {
|
|
17
37
|
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'os-init-codex-'))
|
|
18
38
|
homedirSpy = jest.spyOn(os, 'homedir').mockReturnValue(tempHome)
|
|
39
|
+
originalFetch = global.fetch
|
|
40
|
+
mockCatalogFetch()
|
|
19
41
|
})
|
|
20
42
|
|
|
21
43
|
afterEach(() => {
|
|
22
44
|
homedirSpy.mockRestore()
|
|
45
|
+
if (originalFetch === undefined) {
|
|
46
|
+
global.fetch = undefined as unknown as typeof global.fetch
|
|
47
|
+
} else {
|
|
48
|
+
global.fetch = originalFetch
|
|
49
|
+
}
|
|
23
50
|
fs.rmSync(tempHome, { recursive: true, force: true })
|
|
24
51
|
})
|
|
25
52
|
|
|
26
|
-
test('writes config with 128k auto compact threshold', () => {
|
|
27
|
-
const result = writeCodexConfig('test-api-key')
|
|
53
|
+
test('writes config with 128k auto compact threshold', async () => {
|
|
54
|
+
const result = await writeCodexConfig('test-api-key')
|
|
28
55
|
const config = TOML.parse(fs.readFileSync(result.configPath, 'utf8')) as {
|
|
29
56
|
model_auto_compact_token_limit: number
|
|
57
|
+
model_catalog_json: string
|
|
30
58
|
}
|
|
31
59
|
|
|
32
60
|
expect(config.model_auto_compact_token_limit).toBe(131072)
|
|
61
|
+
expect(config.model_catalog_json).toBe(getExpectedModelCatalogConfigPath())
|
|
62
|
+
expect(fs.existsSync(result.catalogPath)).toBe(true)
|
|
33
63
|
})
|
|
34
64
|
|
|
35
|
-
test('merges template keys and keeps custom config', () => {
|
|
65
|
+
test('merges template keys and keeps custom config', async () => {
|
|
36
66
|
const configDir = path.join(tempHome, '.codex')
|
|
37
67
|
fs.mkdirSync(configDir, { recursive: true })
|
|
38
68
|
const configPath = path.join(configDir, 'config.toml')
|
|
@@ -48,11 +78,12 @@ custom_model = "keep-me"
|
|
|
48
78
|
`
|
|
49
79
|
)
|
|
50
80
|
|
|
51
|
-
writeCodexConfig('test-api-key')
|
|
81
|
+
await writeCodexConfig('test-api-key')
|
|
52
82
|
const config = TOML.parse(fs.readFileSync(configPath, 'utf8')) as {
|
|
53
83
|
service_tier: string
|
|
54
84
|
custom_flag: boolean
|
|
55
85
|
model: string
|
|
86
|
+
model_catalog_json: string
|
|
56
87
|
preferred_auth_method: string
|
|
57
88
|
model_providers: {
|
|
58
89
|
jw: {
|
|
@@ -66,6 +97,7 @@ custom_model = "keep-me"
|
|
|
66
97
|
expect(config.service_tier).toBe('fast')
|
|
67
98
|
expect(config.custom_flag).toBe(true)
|
|
68
99
|
expect(config.model).toBe('gpt-5.4')
|
|
100
|
+
expect(config.model_catalog_json).toBe(getExpectedModelCatalogConfigPath())
|
|
69
101
|
expect(config.preferred_auth_method).toBe('apikey')
|
|
70
102
|
expect(config.model_providers.jw.base_url).toBe(
|
|
71
103
|
'https://ai.gengjiawen.com/api/openai'
|
|
@@ -74,7 +106,7 @@ custom_model = "keep-me"
|
|
|
74
106
|
expect(config.model_providers.jw.name).toBe('jw')
|
|
75
107
|
})
|
|
76
108
|
|
|
77
|
-
test('adds missing keys without removing custom config', () => {
|
|
109
|
+
test('adds missing keys without removing custom config', async () => {
|
|
78
110
|
const configDir = path.join(tempHome, '.codex')
|
|
79
111
|
fs.mkdirSync(configDir, { recursive: true })
|
|
80
112
|
const configPath = path.join(configDir, 'config.toml')
|
|
@@ -88,9 +120,10 @@ base_url = "https://example.com"
|
|
|
88
120
|
`
|
|
89
121
|
)
|
|
90
122
|
|
|
91
|
-
writeCodexConfig('test-api-key')
|
|
123
|
+
await writeCodexConfig('test-api-key')
|
|
92
124
|
const config = TOML.parse(fs.readFileSync(configPath, 'utf8')) as {
|
|
93
125
|
model: string
|
|
126
|
+
model_catalog_json: string
|
|
94
127
|
preferred_auth_method: string
|
|
95
128
|
service_tier: string
|
|
96
129
|
model_providers: {
|
|
@@ -102,8 +135,83 @@ base_url = "https://example.com"
|
|
|
102
135
|
}
|
|
103
136
|
|
|
104
137
|
expect(config.model).toBe('gpt-5.4')
|
|
138
|
+
expect(config.model_catalog_json).toBe(getExpectedModelCatalogConfigPath())
|
|
105
139
|
expect(config.preferred_auth_method).toBe('apikey')
|
|
106
140
|
expect(config.model_providers.jw.name).toBe('jw')
|
|
107
141
|
expect(config.service_tier).toBe('fast')
|
|
108
142
|
})
|
|
143
|
+
|
|
144
|
+
test('refreshes remote model catalog after writing config', async () => {
|
|
145
|
+
const fetchMock = mockCatalogFetch({
|
|
146
|
+
models: [
|
|
147
|
+
{ id: 'gpt-5.4' },
|
|
148
|
+
{ id: 'gpt-5.4-mini' },
|
|
149
|
+
{ id: 'gpt-5.3-codex' },
|
|
150
|
+
],
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const result = await writeCodexConfig('test-api-key')
|
|
154
|
+
|
|
155
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
156
|
+
'https://ai.gengjiawen.com/api/openai/models',
|
|
157
|
+
{
|
|
158
|
+
headers: {
|
|
159
|
+
Accept: 'application/json',
|
|
160
|
+
Authorization: 'Bearer test-api-key',
|
|
161
|
+
'User-Agent': '@gengjiawen/os-init',
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
const catalog = JSON.parse(fs.readFileSync(result.catalogPath, 'utf8')) as {
|
|
167
|
+
models: Array<{ id: string }>
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
expect(catalog).toEqual({
|
|
171
|
+
models: [
|
|
172
|
+
{ id: 'gpt-5.4' },
|
|
173
|
+
{ id: 'gpt-5.4-mini' },
|
|
174
|
+
{ id: 'gpt-5.3-codex' },
|
|
175
|
+
],
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('writes an absolute model catalog path on Windows', async () => {
|
|
180
|
+
const platformSpy = jest.spyOn(os, 'platform').mockReturnValue('win32')
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const result = await writeCodexConfig('test-api-key')
|
|
184
|
+
const config = TOML.parse(fs.readFileSync(result.configPath, 'utf8')) as {
|
|
185
|
+
model_catalog_json: string
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
expect(config.model_catalog_json).toBe(
|
|
189
|
+
path.join(tempHome, '.codex', 'remote-model-catalog.json')
|
|
190
|
+
)
|
|
191
|
+
} finally {
|
|
192
|
+
platformSpy.mockRestore()
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('throws when refreshing the remote model catalog fails', async () => {
|
|
197
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
198
|
+
ok: false,
|
|
199
|
+
status: 503,
|
|
200
|
+
statusText: 'Service Unavailable',
|
|
201
|
+
}) as unknown as typeof global.fetch
|
|
202
|
+
|
|
203
|
+
await expect(writeCodexConfig('test-api-key')).rejects.toThrow(
|
|
204
|
+
'Failed to refresh Codex model catalog: 503 Service Unavailable'
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
expect(fs.existsSync(path.join(tempHome, '.codex', 'config.toml'))).toBe(
|
|
208
|
+
false
|
|
209
|
+
)
|
|
210
|
+
expect(fs.existsSync(path.join(tempHome, '.codex', 'auth.json'))).toBe(
|
|
211
|
+
false
|
|
212
|
+
)
|
|
213
|
+
expect(
|
|
214
|
+
fs.existsSync(path.join(tempHome, '.codex', 'remote-model-catalog.json'))
|
|
215
|
+
).toBe(false)
|
|
216
|
+
})
|
|
109
217
|
})
|
package/libs/codex.ts
CHANGED
|
@@ -6,14 +6,29 @@ import { execa } from 'execa'
|
|
|
6
6
|
import { ensureDir, commandExists, PNPM_INSTALL_ENV } from './utils'
|
|
7
7
|
|
|
8
8
|
type TomlTable = ReturnType<typeof TOML.parse>
|
|
9
|
+
type CodexModelCatalog = { models: unknown[] }
|
|
10
|
+
|
|
11
|
+
const CODEX_BASE_URL = 'https://ai.gengjiawen.com/api/openai'
|
|
12
|
+
const CODEX_MODEL_CATALOG_CONFIG_PATH = '~/.codex/remote-model-catalog.json'
|
|
13
|
+
const CODEX_MODEL_CATALOG_FILENAME = 'remote-model-catalog.json'
|
|
9
14
|
|
|
10
15
|
/** Return Codex configuration directory path */
|
|
11
16
|
function getCodexConfigDir(): string {
|
|
12
17
|
return path.join(os.homedir(), '.codex')
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
function getCodexModelCatalogPath(): string {
|
|
21
|
+
return path.join(getCodexConfigDir(), CODEX_MODEL_CATALOG_FILENAME)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getCodexModelCatalogConfigPath(): string {
|
|
25
|
+
return os.platform() === 'win32'
|
|
26
|
+
? getCodexModelCatalogPath()
|
|
27
|
+
: CODEX_MODEL_CATALOG_CONFIG_PATH
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getCodexConfigTomlTemplate(): string {
|
|
31
|
+
return `model_provider = "jw"
|
|
17
32
|
model = "gpt-5.4"
|
|
18
33
|
model_reasoning_effort = "high"
|
|
19
34
|
plan_mode_reasoning_effort = "xhigh"
|
|
@@ -21,12 +36,14 @@ model_auto_compact_token_limit = 131072
|
|
|
21
36
|
disable_response_storage = true
|
|
22
37
|
preferred_auth_method = "apikey"
|
|
23
38
|
service_tier = "fast"
|
|
39
|
+
model_catalog_json = ${JSON.stringify(getCodexModelCatalogConfigPath())}
|
|
24
40
|
|
|
25
41
|
[model_providers.jw]
|
|
26
42
|
name = "jw"
|
|
27
|
-
base_url = "
|
|
43
|
+
base_url = "${CODEX_BASE_URL}"
|
|
28
44
|
wire_api = "responses"
|
|
29
45
|
`
|
|
46
|
+
}
|
|
30
47
|
|
|
31
48
|
function isTomlTable(value: unknown): value is TomlTable {
|
|
32
49
|
return (
|
|
@@ -57,30 +74,66 @@ function mergeTomlTables(
|
|
|
57
74
|
|
|
58
75
|
function getMergedCodexConfig(existingContent: string): string {
|
|
59
76
|
const existingConfig = TOML.parse(existingContent) as TomlTable
|
|
60
|
-
const templateConfig = TOML.parse(
|
|
77
|
+
const templateConfig = TOML.parse(getCodexConfigTomlTemplate()) as TomlTable
|
|
61
78
|
|
|
62
79
|
return TOML.stringify(mergeTomlTables(existingConfig, templateConfig))
|
|
63
80
|
}
|
|
64
81
|
|
|
82
|
+
async function refreshCodexModelCatalog(apiKey: string): Promise<string> {
|
|
83
|
+
if (typeof fetch !== 'function') {
|
|
84
|
+
throw new Error('Global fetch API is unavailable in this Node.js runtime.')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const response = await fetch(`${CODEX_BASE_URL}/models`, {
|
|
88
|
+
headers: {
|
|
89
|
+
Accept: 'application/json',
|
|
90
|
+
Authorization: `Bearer ${apiKey}`,
|
|
91
|
+
'User-Agent': '@gengjiawen/os-init',
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Failed to refresh Codex model catalog: ${response.status} ${response.statusText}`
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const catalog = (await response.json()) as CodexModelCatalog
|
|
102
|
+
|
|
103
|
+
if (!Array.isArray(catalog?.models) || catalog.models.length === 0) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
'Failed to refresh Codex model catalog: response does not contain any models.'
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const catalogPath = getCodexModelCatalogPath()
|
|
110
|
+
fs.writeFileSync(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`)
|
|
111
|
+
|
|
112
|
+
return catalogPath
|
|
113
|
+
}
|
|
114
|
+
|
|
65
115
|
/** Write Codex config.toml and auth.json */
|
|
66
|
-
export function writeCodexConfig(apiKey: string): {
|
|
116
|
+
export async function writeCodexConfig(apiKey: string): Promise<{
|
|
67
117
|
configPath: string
|
|
68
118
|
authPath: string
|
|
69
|
-
|
|
119
|
+
catalogPath: string
|
|
120
|
+
}> {
|
|
70
121
|
const configDir = getCodexConfigDir()
|
|
71
122
|
ensureDir(configDir)
|
|
72
123
|
|
|
124
|
+
const catalogPath = await refreshCodexModelCatalog(apiKey)
|
|
125
|
+
|
|
73
126
|
const configPath = path.join(configDir, 'config.toml')
|
|
74
127
|
const configContent = fs.existsSync(configPath)
|
|
75
128
|
? getMergedCodexConfig(fs.readFileSync(configPath, 'utf8'))
|
|
76
|
-
:
|
|
129
|
+
: getCodexConfigTomlTemplate()
|
|
77
130
|
fs.writeFileSync(configPath, configContent)
|
|
78
131
|
|
|
79
132
|
const authPath = path.join(configDir, 'auth.json')
|
|
80
133
|
const authContent = JSON.stringify({ OPENAI_API_KEY: apiKey }, null, 2)
|
|
81
134
|
fs.writeFileSync(authPath, authContent)
|
|
82
135
|
|
|
83
|
-
return { configPath, authPath }
|
|
136
|
+
return { configPath, authPath, catalogPath }
|
|
84
137
|
}
|
|
85
138
|
|
|
86
139
|
/** Install Codex dependency */
|
package/libs/opencode.test.ts
CHANGED
|
@@ -12,6 +12,15 @@ describe('writeOpencodeConfig', () => {
|
|
|
12
12
|
let tempHome: string
|
|
13
13
|
let homedirSpy: jest.SpiedFunction<typeof os.homedir>
|
|
14
14
|
|
|
15
|
+
const expectedModelConfig = (name: string) => ({
|
|
16
|
+
name,
|
|
17
|
+
attachment: true,
|
|
18
|
+
modalities: {
|
|
19
|
+
input: ['text', 'image'],
|
|
20
|
+
output: ['text'],
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
15
24
|
beforeEach(() => {
|
|
16
25
|
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'os-init-opencode-'))
|
|
17
26
|
homedirSpy = jest.spyOn(os, 'homedir').mockReturnValue(tempHome)
|
|
@@ -40,15 +49,24 @@ describe('writeOpencodeConfig', () => {
|
|
|
40
49
|
'https://ai.gengjiawen.com/api/openai/v1'
|
|
41
50
|
)
|
|
42
51
|
expect(config.provider.MyCustomProvider.options.apiKey).toBe('test-api-key')
|
|
43
|
-
expect(config.provider.MyCustomProvider.models.code).toEqual(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
expect(config.provider.MyCustomProvider.models.glm).toEqual(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
expect(config.provider.MyCustomProvider.models.kimi).toEqual(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
expect(config.provider.MyCustomProvider.models.code).toEqual(
|
|
53
|
+
expectedModelConfig('code')
|
|
54
|
+
)
|
|
55
|
+
expect(config.provider.MyCustomProvider.models.glm).toEqual(
|
|
56
|
+
expectedModelConfig('glm')
|
|
57
|
+
)
|
|
58
|
+
expect(config.provider.MyCustomProvider.models.kimi).toEqual(
|
|
59
|
+
expectedModelConfig('kimi')
|
|
60
|
+
)
|
|
61
|
+
expect(config.provider.MyCustomProvider.models.minimax).toEqual(
|
|
62
|
+
expectedModelConfig('minimax')
|
|
63
|
+
)
|
|
64
|
+
expect(config.provider.MyCustomProvider.models.deepseek).toEqual(
|
|
65
|
+
expectedModelConfig('deepseek')
|
|
66
|
+
)
|
|
67
|
+
expect(config.provider.MyCustomProvider.models['gemini-flash']).toEqual(
|
|
68
|
+
expectedModelConfig('gemini-flash')
|
|
69
|
+
)
|
|
52
70
|
expect(config.model).toBe('MyCustomProvider/code')
|
|
53
71
|
expect(config.small_model).toBe('MyCustomProvider/code')
|
|
54
72
|
})
|
package/libs/opencode.ts
CHANGED
|
@@ -6,10 +6,36 @@ import { commandExists, ensureDir, PNPM_INSTALL_ENV } from './utils'
|
|
|
6
6
|
|
|
7
7
|
const OPENCODE_PROVIDER_ID = 'MyCustomProvider'
|
|
8
8
|
const OPENCODE_MODEL_ID = 'code'
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
const OPENCODE_MODEL_IDS = [
|
|
10
|
+
OPENCODE_MODEL_ID,
|
|
11
|
+
'glm',
|
|
12
|
+
'kimi',
|
|
13
|
+
'minimax',
|
|
14
|
+
'deepseek',
|
|
15
|
+
'gemini-flash',
|
|
16
|
+
]
|
|
11
17
|
const OPENCODE_BASE_URL = 'https://ai.gengjiawen.com/api/openai/v1'
|
|
12
18
|
|
|
19
|
+
function createOpencodeModelConfig(modelId: string) {
|
|
20
|
+
return {
|
|
21
|
+
name: modelId,
|
|
22
|
+
attachment: true,
|
|
23
|
+
modalities: {
|
|
24
|
+
input: ['text', 'image'],
|
|
25
|
+
output: ['text'],
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createOpencodeModelsConfig() {
|
|
31
|
+
return Object.fromEntries(
|
|
32
|
+
OPENCODE_MODEL_IDS.map((modelId) => [
|
|
33
|
+
modelId,
|
|
34
|
+
createOpencodeModelConfig(modelId),
|
|
35
|
+
])
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
/** Return OpenCode configuration directory path */
|
|
14
40
|
function getOpencodeConfigDir(): string {
|
|
15
41
|
return path.join(os.homedir(), '.config', 'opencode')
|
|
@@ -30,17 +56,7 @@ export function writeOpencodeConfig(apiKey: string): { configPath: string } {
|
|
|
30
56
|
baseURL: OPENCODE_BASE_URL,
|
|
31
57
|
apiKey,
|
|
32
58
|
},
|
|
33
|
-
models:
|
|
34
|
-
[OPENCODE_MODEL_ID]: {
|
|
35
|
-
name: OPENCODE_MODEL_ID,
|
|
36
|
-
},
|
|
37
|
-
[OPENCODE_GLM_MODEL_ID]: {
|
|
38
|
-
name: OPENCODE_GLM_MODEL_ID,
|
|
39
|
-
},
|
|
40
|
-
[OPENCODE_KIMI_MODEL_ID]: {
|
|
41
|
-
name: OPENCODE_KIMI_MODEL_ID,
|
|
42
|
-
},
|
|
43
|
-
},
|
|
59
|
+
models: createOpencodeModelsConfig(),
|
|
44
60
|
},
|
|
45
61
|
},
|
|
46
62
|
model: `${OPENCODE_PROVIDER_ID}/${OPENCODE_MODEL_ID}`,
|