@gengjiawen/os-init 1.18.1 → 1.19.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.19.0](https://github.com/gengjiawen/os-init/compare/v1.18.1...v1.19.0) (2026-03-19)
4
+
5
+
6
+ ### Features
7
+
8
+ * refresh codex model catalog during setup ([9ed724a](https://github.com/gengjiawen/os-init/commit/9ed724accda9901a813bb9265895db6879d406bb))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * add model metadata to opencode config ([59ade2c](https://github.com/gengjiawen/os-init/commit/59ade2c9e9daa307de56d30968a1c3e8977ea945))
14
+
3
15
  ## [1.18.1](https://github.com/gengjiawen/os-init/compare/v1.18.0...v1.18.1) (2026-03-16)
4
16
 
5
17
 
package/bin/bin.js CHANGED
@@ -65,9 +65,11 @@ program
65
65
  return
66
66
  }
67
67
  try {
68
- const { configPath, authPath } = writeCodexConfig(apiKey)
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}`)
@@ -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>;
@@ -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/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,17 @@ 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
- const CODEX_CONFIG_TOML_TEMPLATE = `model_provider = "jw"
17
+ function getCodexModelCatalogPath() {
18
+ return path.join(getCodexConfigDir(), CODEX_MODEL_CATALOG_FILENAME);
19
+ }
20
+ function getCodexConfigTomlTemplate() {
21
+ return `model_provider = "jw"
15
22
  model = "gpt-5.4"
16
23
  model_reasoning_effort = "high"
17
24
  plan_mode_reasoning_effort = "xhigh"
@@ -19,12 +26,14 @@ model_auto_compact_token_limit = 131072
19
26
  disable_response_storage = true
20
27
  preferred_auth_method = "apikey"
21
28
  service_tier = "fast"
29
+ model_catalog_json = "${CODEX_MODEL_CATALOG_CONFIG_PATH}"
22
30
 
23
31
  [model_providers.jw]
24
32
  name = "jw"
25
- base_url = "https://ai.gengjiawen.com/api/openai"
33
+ base_url = "${CODEX_BASE_URL}"
26
34
  wire_api = "responses"
27
35
  `;
36
+ }
28
37
  function isTomlTable(value) {
29
38
  return (value !== undefined &&
30
39
  typeof value === 'object' &&
@@ -44,21 +53,44 @@ function mergeTomlTables(existingConfig, templateConfig) {
44
53
  }
45
54
  function getMergedCodexConfig(existingContent) {
46
55
  const existingConfig = TOML.parse(existingContent);
47
- const templateConfig = TOML.parse(CODEX_CONFIG_TOML_TEMPLATE);
56
+ const templateConfig = TOML.parse(getCodexConfigTomlTemplate());
48
57
  return TOML.stringify(mergeTomlTables(existingConfig, templateConfig));
49
58
  }
50
- function writeCodexConfig(apiKey) {
59
+ async function refreshCodexModelCatalog(apiKey) {
60
+ if (typeof fetch !== 'function') {
61
+ throw new Error('Global fetch API is unavailable in this Node.js runtime.');
62
+ }
63
+ const response = await fetch(`${CODEX_BASE_URL}/models`, {
64
+ headers: {
65
+ Accept: 'application/json',
66
+ Authorization: `Bearer ${apiKey}`,
67
+ 'User-Agent': '@gengjiawen/os-init',
68
+ },
69
+ });
70
+ if (!response.ok) {
71
+ throw new Error(`Failed to refresh Codex model catalog: ${response.status} ${response.statusText}`);
72
+ }
73
+ const catalog = (await response.json());
74
+ if (!Array.isArray(catalog?.models) || catalog.models.length === 0) {
75
+ throw new Error('Failed to refresh Codex model catalog: response does not contain any models.');
76
+ }
77
+ const catalogPath = getCodexModelCatalogPath();
78
+ fs.writeFileSync(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
79
+ return catalogPath;
80
+ }
81
+ async function writeCodexConfig(apiKey) {
51
82
  const configDir = getCodexConfigDir();
52
83
  (0, utils_1.ensureDir)(configDir);
84
+ const catalogPath = await refreshCodexModelCatalog(apiKey);
53
85
  const configPath = path.join(configDir, 'config.toml');
54
86
  const configContent = fs.existsSync(configPath)
55
87
  ? getMergedCodexConfig(fs.readFileSync(configPath, 'utf8'))
56
- : CODEX_CONFIG_TOML_TEMPLATE;
88
+ : getCodexConfigTomlTemplate();
57
89
  fs.writeFileSync(configPath, configContent);
58
90
  const authPath = path.join(configDir, 'auth.json');
59
91
  const authContent = JSON.stringify({ OPENAI_API_KEY: apiKey }, null, 2);
60
92
  fs.writeFileSync(authPath, authContent);
61
- return { configPath, authPath };
93
+ return { configPath, authPath, catalogPath };
62
94
  }
63
95
  async function installCodexDeps() {
64
96
  const packages = ['@openai/codex'];
package/build/opencode.js CHANGED
@@ -12,6 +12,16 @@ const OPENCODE_MODEL_ID = 'code';
12
12
  const OPENCODE_GLM_MODEL_ID = 'glm';
13
13
  const OPENCODE_KIMI_MODEL_ID = 'kimi';
14
14
  const OPENCODE_BASE_URL = 'https://ai.gengjiawen.com/api/openai/v1';
15
+ function createOpencodeModelConfig(modelId) {
16
+ return {
17
+ name: modelId,
18
+ attachment: true,
19
+ modalities: {
20
+ input: ['text', 'image'],
21
+ output: ['text'],
22
+ },
23
+ };
24
+ }
15
25
  function getOpencodeConfigDir() {
16
26
  return path.join(os.homedir(), '.config', 'opencode');
17
27
  }
@@ -29,15 +39,9 @@ function writeOpencodeConfig(apiKey) {
29
39
  apiKey,
30
40
  },
31
41
  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
- },
42
+ [OPENCODE_MODEL_ID]: createOpencodeModelConfig(OPENCODE_MODEL_ID),
43
+ [OPENCODE_GLM_MODEL_ID]: createOpencodeModelConfig(OPENCODE_GLM_MODEL_ID),
44
+ [OPENCODE_KIMI_MODEL_ID]: createOpencodeModelConfig(OPENCODE_KIMI_MODEL_ID),
41
45
  },
42
46
  },
43
47
  },
@@ -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)
@@ -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 = {
@@ -12,27 +12,51 @@ 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 mockCatalogFetch(
18
+ catalog: { models: Array<Record<string, unknown>> } = {
19
+ models: [{ id: 'gpt-5.4' }],
20
+ }
21
+ ): jest.Mock {
22
+ const fetchMock = jest.fn().mockResolvedValue({
23
+ ok: true,
24
+ json: async () => catalog,
25
+ })
26
+ global.fetch = fetchMock as unknown as typeof global.fetch
27
+ return fetchMock
28
+ }
15
29
 
16
30
  beforeEach(() => {
17
31
  tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'os-init-codex-'))
18
32
  homedirSpy = jest.spyOn(os, 'homedir').mockReturnValue(tempHome)
33
+ originalFetch = global.fetch
34
+ mockCatalogFetch()
19
35
  })
20
36
 
21
37
  afterEach(() => {
22
38
  homedirSpy.mockRestore()
39
+ if (originalFetch === undefined) {
40
+ global.fetch = undefined as unknown as typeof global.fetch
41
+ } else {
42
+ global.fetch = originalFetch
43
+ }
23
44
  fs.rmSync(tempHome, { recursive: true, force: true })
24
45
  })
25
46
 
26
- test('writes config with 128k auto compact threshold', () => {
27
- const result = writeCodexConfig('test-api-key')
47
+ test('writes config with 128k auto compact threshold', async () => {
48
+ const result = await writeCodexConfig('test-api-key')
28
49
  const config = TOML.parse(fs.readFileSync(result.configPath, 'utf8')) as {
29
50
  model_auto_compact_token_limit: number
51
+ model_catalog_json: string
30
52
  }
31
53
 
32
54
  expect(config.model_auto_compact_token_limit).toBe(131072)
55
+ expect(config.model_catalog_json).toBe('~/.codex/remote-model-catalog.json')
56
+ expect(fs.existsSync(result.catalogPath)).toBe(true)
33
57
  })
34
58
 
35
- test('merges template keys and keeps custom config', () => {
59
+ test('merges template keys and keeps custom config', async () => {
36
60
  const configDir = path.join(tempHome, '.codex')
37
61
  fs.mkdirSync(configDir, { recursive: true })
38
62
  const configPath = path.join(configDir, 'config.toml')
@@ -48,11 +72,12 @@ custom_model = "keep-me"
48
72
  `
49
73
  )
50
74
 
51
- writeCodexConfig('test-api-key')
75
+ await writeCodexConfig('test-api-key')
52
76
  const config = TOML.parse(fs.readFileSync(configPath, 'utf8')) as {
53
77
  service_tier: string
54
78
  custom_flag: boolean
55
79
  model: string
80
+ model_catalog_json: string
56
81
  preferred_auth_method: string
57
82
  model_providers: {
58
83
  jw: {
@@ -66,6 +91,7 @@ custom_model = "keep-me"
66
91
  expect(config.service_tier).toBe('fast')
67
92
  expect(config.custom_flag).toBe(true)
68
93
  expect(config.model).toBe('gpt-5.4')
94
+ expect(config.model_catalog_json).toBe('~/.codex/remote-model-catalog.json')
69
95
  expect(config.preferred_auth_method).toBe('apikey')
70
96
  expect(config.model_providers.jw.base_url).toBe(
71
97
  'https://ai.gengjiawen.com/api/openai'
@@ -74,7 +100,7 @@ custom_model = "keep-me"
74
100
  expect(config.model_providers.jw.name).toBe('jw')
75
101
  })
76
102
 
77
- test('adds missing keys without removing custom config', () => {
103
+ test('adds missing keys without removing custom config', async () => {
78
104
  const configDir = path.join(tempHome, '.codex')
79
105
  fs.mkdirSync(configDir, { recursive: true })
80
106
  const configPath = path.join(configDir, 'config.toml')
@@ -88,9 +114,10 @@ base_url = "https://example.com"
88
114
  `
89
115
  )
90
116
 
91
- writeCodexConfig('test-api-key')
117
+ await writeCodexConfig('test-api-key')
92
118
  const config = TOML.parse(fs.readFileSync(configPath, 'utf8')) as {
93
119
  model: string
120
+ model_catalog_json: string
94
121
  preferred_auth_method: string
95
122
  service_tier: string
96
123
  model_providers: {
@@ -102,8 +129,66 @@ base_url = "https://example.com"
102
129
  }
103
130
 
104
131
  expect(config.model).toBe('gpt-5.4')
132
+ expect(config.model_catalog_json).toBe('~/.codex/remote-model-catalog.json')
105
133
  expect(config.preferred_auth_method).toBe('apikey')
106
134
  expect(config.model_providers.jw.name).toBe('jw')
107
135
  expect(config.service_tier).toBe('fast')
108
136
  })
137
+
138
+ test('refreshes remote model catalog after writing config', async () => {
139
+ const fetchMock = mockCatalogFetch({
140
+ models: [
141
+ { id: 'gpt-5.4' },
142
+ { id: 'gpt-5.4-mini' },
143
+ { id: 'gpt-5.3-codex' },
144
+ ],
145
+ })
146
+
147
+ const result = await writeCodexConfig('test-api-key')
148
+
149
+ expect(fetchMock).toHaveBeenCalledWith(
150
+ 'https://ai.gengjiawen.com/api/openai/models',
151
+ {
152
+ headers: {
153
+ Accept: 'application/json',
154
+ Authorization: 'Bearer test-api-key',
155
+ 'User-Agent': '@gengjiawen/os-init',
156
+ },
157
+ }
158
+ )
159
+
160
+ const catalog = JSON.parse(fs.readFileSync(result.catalogPath, 'utf8')) as {
161
+ models: Array<{ id: string }>
162
+ }
163
+
164
+ expect(catalog).toEqual({
165
+ models: [
166
+ { id: 'gpt-5.4' },
167
+ { id: 'gpt-5.4-mini' },
168
+ { id: 'gpt-5.3-codex' },
169
+ ],
170
+ })
171
+ })
172
+
173
+ test('throws when refreshing the remote model catalog fails', async () => {
174
+ global.fetch = jest.fn().mockResolvedValue({
175
+ ok: false,
176
+ status: 503,
177
+ statusText: 'Service Unavailable',
178
+ }) as unknown as typeof global.fetch
179
+
180
+ await expect(writeCodexConfig('test-api-key')).rejects.toThrow(
181
+ 'Failed to refresh Codex model catalog: 503 Service Unavailable'
182
+ )
183
+
184
+ expect(fs.existsSync(path.join(tempHome, '.codex', 'config.toml'))).toBe(
185
+ false
186
+ )
187
+ expect(fs.existsSync(path.join(tempHome, '.codex', 'auth.json'))).toBe(
188
+ false
189
+ )
190
+ expect(
191
+ fs.existsSync(path.join(tempHome, '.codex', 'remote-model-catalog.json'))
192
+ ).toBe(false)
193
+ })
109
194
  })
package/libs/codex.ts CHANGED
@@ -6,14 +6,23 @@ 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
- /** Template for Codex config.toml */
16
- const CODEX_CONFIG_TOML_TEMPLATE = `model_provider = "jw"
20
+ function getCodexModelCatalogPath(): string {
21
+ return path.join(getCodexConfigDir(), CODEX_MODEL_CATALOG_FILENAME)
22
+ }
23
+
24
+ function getCodexConfigTomlTemplate(): string {
25
+ return `model_provider = "jw"
17
26
  model = "gpt-5.4"
18
27
  model_reasoning_effort = "high"
19
28
  plan_mode_reasoning_effort = "xhigh"
@@ -21,12 +30,14 @@ model_auto_compact_token_limit = 131072
21
30
  disable_response_storage = true
22
31
  preferred_auth_method = "apikey"
23
32
  service_tier = "fast"
33
+ model_catalog_json = "${CODEX_MODEL_CATALOG_CONFIG_PATH}"
24
34
 
25
35
  [model_providers.jw]
26
36
  name = "jw"
27
- base_url = "https://ai.gengjiawen.com/api/openai"
37
+ base_url = "${CODEX_BASE_URL}"
28
38
  wire_api = "responses"
29
39
  `
40
+ }
30
41
 
31
42
  function isTomlTable(value: unknown): value is TomlTable {
32
43
  return (
@@ -57,30 +68,66 @@ function mergeTomlTables(
57
68
 
58
69
  function getMergedCodexConfig(existingContent: string): string {
59
70
  const existingConfig = TOML.parse(existingContent) as TomlTable
60
- const templateConfig = TOML.parse(CODEX_CONFIG_TOML_TEMPLATE) as TomlTable
71
+ const templateConfig = TOML.parse(getCodexConfigTomlTemplate()) as TomlTable
61
72
 
62
73
  return TOML.stringify(mergeTomlTables(existingConfig, templateConfig))
63
74
  }
64
75
 
76
+ async function refreshCodexModelCatalog(apiKey: string): Promise<string> {
77
+ if (typeof fetch !== 'function') {
78
+ throw new Error('Global fetch API is unavailable in this Node.js runtime.')
79
+ }
80
+
81
+ const response = await fetch(`${CODEX_BASE_URL}/models`, {
82
+ headers: {
83
+ Accept: 'application/json',
84
+ Authorization: `Bearer ${apiKey}`,
85
+ 'User-Agent': '@gengjiawen/os-init',
86
+ },
87
+ })
88
+
89
+ if (!response.ok) {
90
+ throw new Error(
91
+ `Failed to refresh Codex model catalog: ${response.status} ${response.statusText}`
92
+ )
93
+ }
94
+
95
+ const catalog = (await response.json()) as CodexModelCatalog
96
+
97
+ if (!Array.isArray(catalog?.models) || catalog.models.length === 0) {
98
+ throw new Error(
99
+ 'Failed to refresh Codex model catalog: response does not contain any models.'
100
+ )
101
+ }
102
+
103
+ const catalogPath = getCodexModelCatalogPath()
104
+ fs.writeFileSync(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`)
105
+
106
+ return catalogPath
107
+ }
108
+
65
109
  /** Write Codex config.toml and auth.json */
66
- export function writeCodexConfig(apiKey: string): {
110
+ export async function writeCodexConfig(apiKey: string): Promise<{
67
111
  configPath: string
68
112
  authPath: string
69
- } {
113
+ catalogPath: string
114
+ }> {
70
115
  const configDir = getCodexConfigDir()
71
116
  ensureDir(configDir)
72
117
 
118
+ const catalogPath = await refreshCodexModelCatalog(apiKey)
119
+
73
120
  const configPath = path.join(configDir, 'config.toml')
74
121
  const configContent = fs.existsSync(configPath)
75
122
  ? getMergedCodexConfig(fs.readFileSync(configPath, 'utf8'))
76
- : CODEX_CONFIG_TOML_TEMPLATE
123
+ : getCodexConfigTomlTemplate()
77
124
  fs.writeFileSync(configPath, configContent)
78
125
 
79
126
  const authPath = path.join(configDir, 'auth.json')
80
127
  const authContent = JSON.stringify({ OPENAI_API_KEY: apiKey }, null, 2)
81
128
  fs.writeFileSync(authPath, authContent)
82
129
 
83
- return { configPath, authPath }
130
+ return { configPath, authPath, catalogPath }
84
131
  }
85
132
 
86
133
  /** Install Codex dependency */
@@ -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,15 @@ 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
- name: 'code',
45
- })
46
- expect(config.provider.MyCustomProvider.models.glm).toEqual({
47
- name: 'glm',
48
- })
49
- expect(config.provider.MyCustomProvider.models.kimi).toEqual({
50
- name: 'kimi',
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
+ )
52
61
  expect(config.model).toBe('MyCustomProvider/code')
53
62
  expect(config.small_model).toBe('MyCustomProvider/code')
54
63
  })
package/libs/opencode.ts CHANGED
@@ -10,6 +10,17 @@ const OPENCODE_GLM_MODEL_ID = 'glm'
10
10
  const OPENCODE_KIMI_MODEL_ID = 'kimi'
11
11
  const OPENCODE_BASE_URL = 'https://ai.gengjiawen.com/api/openai/v1'
12
12
 
13
+ function createOpencodeModelConfig(modelId: string) {
14
+ return {
15
+ name: modelId,
16
+ attachment: true,
17
+ modalities: {
18
+ input: ['text', 'image'],
19
+ output: ['text'],
20
+ },
21
+ }
22
+ }
23
+
13
24
  /** Return OpenCode configuration directory path */
14
25
  function getOpencodeConfigDir(): string {
15
26
  return path.join(os.homedir(), '.config', 'opencode')
@@ -31,15 +42,13 @@ export function writeOpencodeConfig(apiKey: string): { configPath: string } {
31
42
  apiKey,
32
43
  },
33
44
  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
- },
45
+ [OPENCODE_MODEL_ID]: createOpencodeModelConfig(OPENCODE_MODEL_ID),
46
+ [OPENCODE_GLM_MODEL_ID]: createOpencodeModelConfig(
47
+ OPENCODE_GLM_MODEL_ID
48
+ ),
49
+ [OPENCODE_KIMI_MODEL_ID]: createOpencodeModelConfig(
50
+ OPENCODE_KIMI_MODEL_ID
51
+ ),
43
52
  },
44
53
  },
45
54
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gengjiawen/os-init",
3
3
  "private": false,
4
- "version": "1.18.1",
4
+ "version": "1.19.0",
5
5
  "description": "",
6
6
  "main": "index.js",
7
7
  "bin": {