Skip to content

Commit 38bca18

Browse files
lidge-jun0disoft
andcommitted
feat: allow static provider model catalogs
Absorb PR #16 into dev while preserving live model discovery by default and keeping static allowlists out of the live model cache. Co-authored-by: 0disoft <rodisoft1@gmail.com>
1 parent 241e327 commit 38bca18

7 files changed

Lines changed: 351 additions & 6 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# 2026-06-23 PR #16/#22 Dev Integration Plan
2+
3+
## Objective
4+
5+
Absorb the remaining open PRs into `dev` without routing them through the GitHub PR merge button:
6+
7+
- PR #16: static provider model catalogs via `liveModels: false`.
8+
- PR #22: Anthropic Messages tool-result history repair.
9+
10+
The final state must preserve existing live `/models` discovery by default, keep static allowlists opt-in only, fix invalid Anthropic `tool_result` history shapes, push `dev`, and close both superseded PRs with attribution.
11+
12+
## Current State
13+
14+
- Repository: `/Users/jun/Developer/new/700_projects/opencodex`
15+
- Branch: `dev`
16+
- Latest release baseline: `v2.1.7` / `241e327 release: v2.1.7`
17+
- Open PRs:
18+
- `#16 [codex] Allow static provider model catalogs` by `0disoft`, mergeable, CI green.
19+
- `#22 fix anthropic tool result history` by `jaekwonhong`, draft, mergeable, no reported checks.
20+
21+
## Planned Changes
22+
23+
### MODIFY `src/types.ts`
24+
25+
Add optional provider config:
26+
27+
```ts
28+
liveModels?: boolean;
29+
```
30+
31+
Behavior:
32+
33+
- `undefined` and `true` keep the current live `/models` behavior.
34+
- `false` means `models` is an intentional static Codex catalog allowlist.
35+
36+
### MODIFY `src/codex-catalog.ts`
37+
38+
Change `fetchProviderModels` order:
39+
40+
1. Skip forward providers and unresolved OAuth providers as today.
41+
2. Build configured model hints first from `prov.models`.
42+
3. If `prov.liveModels === false`, return configured models immediately.
43+
4. Do not write static allowlists into the live model cache.
44+
5. Otherwise use fresh cache, live `/models`, stale cache, and configured fallback as today.
45+
46+
This preserves the existing dashboard `/api/models`, Codex `/v1/models`, and catalog sync behavior because all three already route through `gatherRoutedModels(config)`.
47+
48+
### MODIFY `tests/codex-catalog.test.ts`
49+
50+
Add/absorb regression tests:
51+
52+
- `liveModels:false` never calls `fetch`.
53+
- A fresh live cache cannot override a static allowlist.
54+
- Static allowlists do not poison the live cache after toggling back to live models.
55+
- Config catalog hints still apply to static configured models, specifically `modelContextWindows`
56+
and `modelInputModalities` on a `liveModels:false` provider.
57+
58+
### MODIFY docs
59+
60+
Update configuration reference pages:
61+
62+
- `/Users/jun/Developer/new/700_projects/opencodex/docs-site/src/content/docs/reference/configuration.md`
63+
- `/Users/jun/Developer/new/700_projects/opencodex/docs-site/src/content/docs/ko/reference/configuration.md`
64+
- `/Users/jun/Developer/new/700_projects/opencodex/docs-site/src/content/docs/zh-cn/reference/configuration.md`
65+
66+
Document:
67+
68+
- `models` remains seed/fallback by default.
69+
- `liveModels:false` makes `models` an exact allowlist.
70+
- Empty `models` with `liveModels:false` exposes no routed models.
71+
72+
### MODIFY `src/adapters/anthropic.ts`
73+
74+
Repair Anthropic tool-result history conversion:
75+
76+
- Adjacent `toolResult` messages immediately following assistant `tool_use` blocks must be folded into one user message containing Anthropic `tool_result` blocks.
77+
- Missing tool results should be represented by synthetic `is_error: true` tool_result blocks so the request stays valid.
78+
- Duplicate or orphan tool results should be preserved as text instead of invalid standalone Anthropic `tool_result` blocks.
79+
- Image/tool content blocks should continue using existing `toAnthropicContentPart`.
80+
81+
### MODIFY `tests/adapter-usage.test.ts`
82+
83+
Add/absorb regression tests:
84+
85+
- Multiple adjacent tool results are folded into one user message.
86+
- Missing tool result is synthesized with `is_error: true`.
87+
- Orphan tool result is preserved as text.
88+
- Duplicate tool results after one matching assistant `tool_use` are preserved as text, not a
89+
second invalid Anthropic `tool_result` block.
90+
- Non-string tool-result content, including image content, still maps through Anthropic content
91+
blocks.
92+
- Existing OpenAI Chat history repair tests remain green.
93+
94+
## Integration Strategy
95+
96+
1. Enter PABCD `A` and dispatch a read-only plan audit.
97+
2. Merge PR #16 into local `dev` using git, preserving contributor history where practical.
98+
3. Merge PR #22 into local `dev`, despite draft status, because the user explicitly authorized absorbing and closing it.
99+
4. Resolve conflicts manually if needed; add small follow-up patch if PR code regresses existing metadata caps or cache semantics.
100+
5. Run focused verification:
101+
- `bun test tests/codex-catalog.test.ts`
102+
- `bun test tests/adapter-usage.test.ts tests/umans-provider.test.ts`
103+
- `bun run typecheck`
104+
6. Run full verification:
105+
- `bun test tests`
106+
- `cd docs-site && bun run build`
107+
- `git diff --check`
108+
7. Commit or preserve merge commits atomically.
109+
8. Push `dev` to `origin/dev`.
110+
9. Comment on PR #16 and PR #22 with absorbed commit/evidence and close them.
111+
10. Record final goal evidence and pause the goal with an independent stop audit.
112+
113+
## Risks
114+
115+
- PR #16 could accidentally make static allowlists the default. Mitigation: tests assert the default live path still fetches.
116+
- PR #16 could pollute the live model cache with static allowlists. Mitigation: cache toggle test.
117+
- PR #22 could alter valid Anthropic history order. Mitigation: focused adapter tests plus existing usage tests.
118+
- PR #22 is draft. Mitigation: do not GitHub-merge the draft PR; absorb into `dev` with explicit user authorization and close as superseded.
119+
120+
## Completion Evidence Required
121+
122+
- Documentation: this devlog plan plus final devlog notes.
123+
- Implementation: changed source/tests/docs paths listed above.
124+
- Verification: focused tests, typecheck, full tests, docs build, diff check.
125+
- GitHub: `origin/dev` updated, PR #16 and #22 commented and closed.

docs-site/src/content/docs/ko/reference/configuration.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ opencodex는 `~/.opencodex/config.json`으로 설정됩니다. 이 파일은 `oc
3333
| `baseUrl` | `string` | 업스트림 API 기본 URL. |
3434
| `apiKey?` | `string` | API 키, 또는 요청 시점에 해석되는 `${ENV_VAR}` / `$ENV_VAR` 참조. |
3535
| `defaultModel?` | `string` | 명시적인 모델 없이 이 프로바이더가 선택되었을 때 사용하는 모델. |
36-
| `models?` | `string[]` | 시드/폴백 모델 목록 (실시간 `/models`에 접근 가능하면 그쪽이 우선됨). |
36+
| `models?` | `string[]` | 시드/폴백 모델 목록. `liveModels``false`이면 Codex 카탈로그에 노출할 정확한 allowlist가 됩니다. |
37+
| `liveModels?` | `boolean` | 시작/동기화 시 프로바이더의 실시간 `/models` 카탈로그를 가져옵니다(기본 `true`). `false`로 두면 설정된 `models`만 사용합니다. |
3738
| `contextWindow?` | `number` | routed catalog entry에 표시할 프로바이더 전체 context-window cap. 실시간 metadata가 이보다 작으면 그대로 둡니다. |
3839
| `modelContextWindows?` | `Record<string,number>` | 모델별 context-window cap. 매칭되는 모델에서는 `contextWindow`보다 우선하며, 더 작은 실시간 metadata를 올리지 않습니다. |
3940
| `modelInputModalities?` | `Record<string,string[]>` | `["text"]` 또는 `["text", "image"]` 같은 모델별 catalog input hint. |
@@ -43,6 +44,28 @@ opencodex는 `~/.opencodex/config.json`으로 설정됩니다. 이 파일은 `oc
4344
| `noVisionModels?` | `string[]` | 텍스트 전용 모델 — [비전 사이드카](/opencodex/ko/guides/sidecars/)가 이들을 위해 이미지를 설명함. 매칭 시 Ollama의 `:size` 태그를 허용함. |
4445
| `escapeBuiltinToolNames?` | `boolean` | Umans 같은 Anthropic 호환 게이트웨이가 wire에서 도구명 escape를 요구할 때 사용합니다. opencodex는 Codex에 tool call을 돌려주기 전에 prefix를 제거합니다. |
4546

47+
## 정적 모델 allowlist
48+
49+
일부 프로바이더는 실시간 모델 카탈로그가 매우 크거나 느릴 수 있습니다. Codex에 `models`
50+
고정한 모델만 노출하려면 `liveModels``false`로 설정하세요.
51+
52+
`liveModels``false`인데 `models`가 비어 있거나 생략되면, opencodex는 해당 프로바이더의
53+
routed model을 노출하지 않습니다.
54+
55+
```json
56+
{
57+
"providers": {
58+
"openrouter": {
59+
"adapter": "openai-chat",
60+
"baseUrl": "https://openrouter.ai/api/v1",
61+
"apiKey": "${OPENROUTER_API_KEY}",
62+
"liveModels": false,
63+
"models": ["deepseek/deepseek-v4-flash", "qwen/qwen3-coder-plus"]
64+
}
65+
}
66+
}
67+
```
68+
4669
## 사이드카
4770

4871
### `webSearchSidecar` (`OcxWebSearchSidecarConfig`)

docs-site/src/content/docs/reference/configuration.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ also force the same native-provider recovery with `ocx recover-history --legacy-
3333
| `baseUrl` | `string` | Upstream API base URL. |
3434
| `apiKey?` | `string` | API key, or an `${ENV_VAR}` / `$ENV_VAR` reference resolved at request time. |
3535
| `defaultModel?` | `string` | Model used when this provider is selected without an explicit model. |
36-
| `models?` | `string[]` | Seed/fallback model list (live `/models` is preferred when reachable). |
36+
| `models?` | `string[]` | Seed/fallback model list. Also becomes the exact catalog allowlist when `liveModels` is `false`. |
37+
| `liveModels?` | `boolean` | Fetch the provider's live `/models` catalog on start/sync (default `true`). Set `false` to use only configured `models`. |
3738
| `contextWindow?` | `number` | Provider-wide Codex-visible context-window cap for routed catalog entries. Live metadata below this value is kept. |
3839
| `modelContextWindows?` | `Record<string,number>` | Model-specific context-window caps. These override `contextWindow` for matching model ids and never raise smaller live metadata. |
3940
| `modelInputModalities?` | `Record<string,string[]>` | Model-specific catalog input hints such as `["text"]` or `["text", "image"]`. |
@@ -43,6 +44,28 @@ also force the same native-provider recovery with `ocx recover-history --legacy-
4344
| `noVisionModels?` | `string[]` | Text-only models — the [vision sidecar](/opencodex/guides/sidecars/) describes images for them. Matching tolerates an Ollama `:size` tag. |
4445
| `escapeBuiltinToolNames?` | `boolean` | Anthropic-compatible gateways such as Umans can require tool-name escaping on the wire; opencodex strips the prefix before returning tool calls to Codex. |
4546

47+
## Static model allowlists
48+
49+
Some providers expose very large or slow live model catalogs. Set `liveModels` to `false` when you
50+
want Codex to see only the models pinned in `models`:
51+
52+
When `liveModels` is `false` and `models` is empty or omitted, opencodex exposes no routed models
53+
for that provider.
54+
55+
```json
56+
{
57+
"providers": {
58+
"openrouter": {
59+
"adapter": "openai-chat",
60+
"baseUrl": "https://openrouter.ai/api/v1",
61+
"apiKey": "${OPENROUTER_API_KEY}",
62+
"liveModels": false,
63+
"models": ["deepseek/deepseek-v4-flash", "qwen/qwen3-coder-plus"]
64+
}
65+
}
66+
}
67+
```
68+
4669
## Sidecars
4770

4871
### `webSearchSidecar` (`OcxWebSearchSidecarConfig`)

docs-site/src/content/docs/zh-cn/reference/configuration.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ opencodex 通过 `~/.opencodex/config.json` 进行配置。它由 `ocx init` 和
3131
| `baseUrl` | `string` | 上游 API 的基础 URL。 |
3232
| `apiKey?` | `string` | API key,或在请求时解析的 `${ENV_VAR}` / `$ENV_VAR` 引用。 |
3333
| `defaultModel?` | `string` | 当选中该 provider 但未指定明确模型时使用的模型。 |
34-
| `models?` | `string[]` | 种子/回退模型列表(当实时 `/models` 可达时优先使用它)。 |
34+
| `models?` | `string[]` | 种子/回退模型列表。当 `liveModels``false` 时,它也是 Codex 目录中精确暴露的 allowlist。 |
35+
| `liveModels?` | `boolean` | 启动/同步时获取 provider 的实时 `/models` 目录(默认 `true`)。设为 `false` 时只使用配置的 `models`|
3536
| `contextWindow?` | `number` | 路由 catalog 条目的 provider 级上下文窗口上限。实时 metadata 小于该值时会保留实时值。 |
3637
| `modelContextWindows?` | `Record<string,number>` | 模型级上下文窗口上限。匹配模型时优先于 `contextWindow`,且不会抬高更小的实时 metadata。 |
3738
| `modelInputModalities?` | `Record<string,string[]>` | 模型级 catalog 输入提示,例如 `["text"]``["text", "image"]`|
@@ -41,6 +42,27 @@ opencodex 通过 `~/.opencodex/config.json` 进行配置。它由 `ocx init` 和
4142
| `noVisionModels?` | `string[]` | 纯文本模型 —— [视觉 sidecar](/opencodex/zh-cn/guides/sidecars/) 会为它们描述图像。匹配时可容忍 Ollama 的 `:size` 标签。 |
4243
| `escapeBuiltinToolNames?` | `boolean` | Umans 等 Anthropic 兼容网关可能要求在 wire 上转义工具名;opencodex 会在把 tool call 返回给 Codex 前移除前缀。 |
4344

45+
## 静态模型 allowlist
46+
47+
有些 provider 的实时模型目录非常大或响应较慢。如果只希望 Codex 看到 `models` 中固定的模型,
48+
可以将 `liveModels` 设为 `false`
49+
50+
`liveModels``false``models` 为空或省略时,opencodex 不会为该 provider 暴露 routed model。
51+
52+
```json
53+
{
54+
"providers": {
55+
"openrouter": {
56+
"adapter": "openai-chat",
57+
"baseUrl": "https://openrouter.ai/api/v1",
58+
"apiKey": "${OPENROUTER_API_KEY}",
59+
"liveModels": false,
60+
"models": ["deepseek/deepseek-v4-flash", "qwen/qwen3-coder-plus"]
61+
}
62+
}
63+
}
64+
```
65+
4466
## Sidecars
4567

4668
### `webSearchSidecar`(`OcxWebSearchSidecarConfig`)

src/codex-catalog.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,13 +419,16 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
419419
if (prov.authMode === "forward") return []; // ChatGPT backend has no /models
420420
const apiKey = await resolveModelsAuthToken(name, prov);
421421
if (prov.authMode === "oauth" && !apiKey) return []; // not logged in → skip
422-
const fresh = getFreshCached(name, ttlMs);
423-
if (fresh) return applyConfigHintsToCachedModels(name, prov, fresh); // dedups Codex's frequent /v1/models polling within the TTL
424422
const configured: CatalogModel[] = (prov.models ?? []).map(id => ({
425423
id,
426424
provider: name,
427425
...catalogHintsFromProviderConfig(name, prov, id),
428426
}));
427+
if (prov.liveModels === false) {
428+
return configured;
429+
}
430+
const fresh = getFreshCached(name, ttlMs);
431+
if (fresh) return applyConfigHintsToCachedModels(name, prov, fresh); // dedups Codex's frequent /v1/models polling within the TTL
429432
const { url, headers } = buildModelsRequest(prov, apiKey);
430433
try {
431434
const res = await fetch(url, { headers, signal: AbortSignal.timeout(8000) });

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,12 @@ export interface OcxProviderConfig {
230230
apiKey?: string;
231231
defaultModel?: string;
232232
models?: string[];
233+
/**
234+
* Fetch the provider's live `/models` endpoint. Defaults to true.
235+
* Set false when `models` is an intentional allowlist or a provider's live catalog is too large
236+
* or too flaky for startup/catalog sync.
237+
*/
238+
liveModels?: boolean;
233239
/** Provider-wide Codex-visible context-window cap for routed catalog entries. */
234240
contextWindow?: number;
235241
/** Model-specific Codex-visible context-window caps. Values cap live metadata, never raise it. */

0 commit comments

Comments
 (0)