Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Persisted provider field:
```ts
initialModelSelection?: {
version: 1;
registrationId: string; // new UUID on first creation only; preserved on overwrite
status: "pending" | "ready" | "all-off";
modelCount?: number;
};
Expand Down Expand Up @@ -84,6 +85,10 @@ No change to explicit model-ID routing.
### NEW src/providers/initial-model-selection-runtime.ts

Own the ordinary-discovery completion write, independent of Codex integration.
Match registration UUID as well as normalized inventory-producing configuration
(including custom rows, combos and provider dependencies). Equal field values
after delete/re-add are not the same registration. Schema-default normalization
and order-independent comparison avoid spurious mismatches after load/save.
Capture pending provider config and disabledModels before gather; use existing
authoritative outcome metadata and the pure transition after discovery. Re-read
under mutatePersistedConfig, compare the captured provider/selection identity,
Expand All @@ -103,16 +108,16 @@ no pending provider keep the existing fast path, with no writes/new discovery.
catalog evidence; never insert config writes inside an already sealed gather.
The evidence-only gather entry point remains mutation-free.

### MODIFY src/codex/convergence.ts
### MODIFY src/codex/management-convergence.ts and src/codex/convergence.ts

Before prepareCatalog, clone snapshot config as now, run initial-selection
reconciliation using authoritative providerModelOutcomes (static included), then
run existing successful-discovery reconciliation; execute BOTH, do not short-circuit
one in an `a || b` call expression. Carry projected config if either changed.
After successful admitted commit, adopt state with disabledModels/modelDiscovery
and use existing coordinated save. A failed/stale/busy commit must not publish
state or OFF decisions. Snapshot identity already hashes complete config, so the
new provider field is covered without a second fingerprint implementation.
Implementation refinement: the management wrapper resolves pending initialization
BEFORE capturing catalog admission, just as retained sync does before its evidence
read. This avoids coupling durable initial selection to a later catalog-file write
or exposing an in-memory completed marker after a failed config save. The evidence
gather stays read-only; convergence only carries pending-provider names into final
visibility filtering. Existing later-arrival projection is untouched. Registration
choices commit independently of optional Codex catalog success. Snapshot identity
already hashes complete config, so no second fingerprint implementation is needed.

### MODIFY src/server/management/model-rows.ts and model-routes.ts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ description: Entrées du fournisseur, authentification, points de terminaison, c
Un fournisseur indique à opencodex où se trouve un modèle, quel adaptateur de protocole il utilise et comment les requêtes sont
authentifiées.

## Sélection des modèles à l’inscription

Une nouvelle connexion sans OAuth attend une liste de modèles fiable avant de les exposer. Si l’onglet Models contient au moins 20 lignes distinctes, tous les interrupteurs de modèles sont initialement OFF ; le fournisseur reste ACTIVE. Les connexions utilisant effectivement OAuth ou la connexion ChatGPT conservent leurs valeurs par défaut.

Cette règle ne s’applique qu’à l’inscription d’un nouveau fournisseur. Les mises à jour, reconnexions et remplacements de clé préservent les choix existants. Après l’initialisation, activez les modèles souhaités dans Models ou avec les commandes ci-dessous. La politique distincte concernant les nouveaux modèles reste inchangée. Remplacez `<model-id>` par un ID de la liste.

```sh
ocx models live --provider openrouter
ocx models enable 'openrouter/<model-id>'
ocx models disable 'openrouter/<model-id>'
ocx models provider openrouter on
```

## Champs de premier niveau liés aux fournisseurs

| Champ | Type | Par défaut | Signification |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ description: プロバイダー エントリ、認証、エンドポイント、

プロバイダーは、opencodex に、モデルが存在する場所、モデルが通信するワイヤー アダプター、およびリクエストの認証方法を伝えます。

## 初回登録時のモデル選択

新しい非 OAuth 接続では、信頼できるモデル一覧の取得が完了するまでモデルの公開を保留します。Models タブの重複しないモデル行が20個以上なら、モデルのスイッチをすべて OFF にします。プロバイダー自体は ACTIVE のままです。実際の認証方式が OAuth または ChatGPT ログインなら既定値を維持します。

初回のプロバイダー登録にのみ適用され、更新、再ログイン、キー交換で既存の選択をリセットしません。初期設定後は Models または以下の CLI で必要なモデルを有効にできます。後から追加されるモデルのポリシーは変更しません。`<model-id>` を一覧の ID に置き換えてください。

```sh
ocx models live --provider openrouter
ocx models enable 'openrouter/<model-id>'
ocx models disable 'openrouter/<model-id>'
ocx models provider openrouter on
```

## プロバイダー関連のトップレベルフィールド

|フィールド |タイプ |デフォルト |意味 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ description: 공급자 항목, 인증, 엔드포인트, 모델 카탈로그, 할

공급자는 opencodex에 모델의 위치, 사용하는 와이어 어댑터, 요청 인증 방식을 알려줍니다.

## 처음 등록할 때의 모델 선택

신규 비-OAuth 연결은 모델 목록 조회가 끝날 때까지 모델 노출을 보류합니다. Models 탭의 중복 없는 모델 행이 20개 이상이면 모델 스위치를 모두 OFF로 설정합니다. 프로바이더는 활성 상태를 유지합니다. 실제 인증 방식이 OAuth나 ChatGPT 로그인인 연결은 기존 기본값을 유지합니다.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

처음 등록할 때만 적용하며 업데이트, 재로그인, 키 교체로 기존 선택을 초기화하지 않습니다. 초기 설정이 끝나면 Models 탭이나 아래 CLI 명령으로 필요한 모델을 켤 수 있습니다. 이후 새 모델이 추가될 때의 정책은 별도입니다. `<model-id>`는 목록에 나온 ID로 바꾸세요.

```sh
ocx models live --provider openrouter
ocx models enable 'openrouter/<model-id>'
ocx models disable 'openrouter/<model-id>'
ocx models provider openrouter on
```

## 공급자 관련 최상위 필드

| 필드 | 타입 | 기본값 | 의미 |
Expand Down
13 changes: 13 additions & 0 deletions docs-site/src/content/docs/reference/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ description: Provider entries, authentication, endpoints, model catalogs, quotas
A provider tells opencodex where a model lives, which wire adapter it speaks, and how requests are
authenticated.

## Initial model selection

New non-OAuth connections wait for a reliable model list before exposing models. If that list contains at least 20 distinct Models-tab rows, all model switches start OFF; the provider itself stays ACTIVE. OAuth and ChatGPT-login connections keep their defaults, based on the effective authentication mode.

This runs only for a new provider registration. Existing selections survive updates, re-login and key replacement. After initialization, enable the models you need in Models or with the CLI below; the separate new-model-arrival policy is unchanged. Replace `<model-id>` with an ID from the list.

```sh
ocx models live --provider openrouter
ocx models enable 'openrouter/<model-id>'
ocx models disable 'openrouter/<model-id>'
ocx models provider openrouter on
```

## Provider-related top-level fields

| Field | Type | Default | Meaning |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ description: Записи провайдеров, аутентификация,
Провайдер сообщает opencodex, где живёт модель, на каком wire-adapter'е она работает и как
аутентифицируются запросы.

## Выбор моделей при первой регистрации

Новое подключение без OAuth не публикует модели до получения достоверного списка. Если в Models не менее 20 уникальных строк моделей, все переключатели моделей изначально OFF, но сам провайдер остаётся ACTIVE. Подключения, фактически использующие OAuth или вход ChatGPT, сохраняют настройки по умолчанию.

Правило действует только при регистрации нового провайдера. Обновления, повторный вход и смена ключа не сбрасывают существующий выбор. После инициализации включите нужные модели в Models или командами ниже. Отдельная политика появления новых моделей не меняется. Замените `<model-id>` на ID из списка.

```sh
ocx models live --provider openrouter
ocx models enable 'openrouter/<model-id>'
ocx models disable 'openrouter/<model-id>'
ocx models provider openrouter on
```

## Верхнеуровневые поля, связанные с провайдерами

| Поле | Тип | По умолчанию | Значение |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ description: Sağlayıcı girdileri, kimlik doğrulama, uç noktalar, model kata
Bir sağlayıcı, opencodex'e bir modelin nerede yaşadığını, hangi hat adaptörünü
konuştuğunu ve isteklerin nasıl doğrulandığını söyler.

## İlk kayıtta model seçimi

Yeni OAuth dışı bağlantılar, modelleri göstermeden önce güvenilir bir model listesini bekler. Models sekmesinde en az 20 benzersiz model satırı varsa tüm model anahtarları başlangıçta OFF olur; sağlayıcının kendisi ACTIVE kalır. Gerçekte OAuth veya ChatGPT girişi kullanan bağlantılar varsayılanlarını korur.

Bu kural yalnızca yeni sağlayıcı kaydında uygulanır. Güncellemeler, yeniden giriş ve anahtar değişimi mevcut seçimleri sıfırlamaz. İlk ayardan sonra gerekli modelleri Models üzerinden veya aşağıdaki CLI komutlarıyla açın. Sonradan gelen yeni modellerin ayrı politikası değişmez. `<model-id>` yerine listedeki bir ID yazın.

```sh
ocx models live --provider openrouter
ocx models enable 'openrouter/<model-id>'
ocx models disable 'openrouter/<model-id>'
ocx models provider openrouter on
```

## Sağlayıcı ile ilgili üst düzey alanlar

| Alan | Tip | Varsayılan | Anlamı |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ description: 提供者条目、身份验证、端点、模型目录、配额、

提供者用于告诉 opencodex 模型位于哪里、使用哪种线协议适配器,以及请求如何进行身份验证。

## 首次注册时的模型选择

新的非 OAuth 连接会等待可靠的模型列表,再公开模型。如果 Models 标签页中去重后的模型行达到20个,所有模型开关初始为 OFF,但提供者本身保持 ACTIVE。实际认证方式为 OAuth 或 ChatGPT 登录的连接保留默认设置。

仅在首次注册提供者时应用;更新、重新登录和更换密钥不会重置已有选择。初始化后,可在 Models 或使用以下 CLI 命令启用所需模型。后续新增模型的独立策略不变。请将 `<model-id>` 替换为列表中的 ID。

```sh
ocx models live --provider openrouter
ocx models enable 'openrouter/<model-id>'
ocx models disable 'openrouter/<model-id>'
ocx models provider openrouter on
```

## 提供者相关顶级字段

| 字段 | 类型 | 默认值 | 含义 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ description: 供應商項目、認證、端點、模型目錄、配額、context

供應商告訴 opencodex 模型在哪裡、它使用哪種 wire adapter,以及請求如何被認證。

## 首次註冊時的模型選擇

新的非 OAuth 連線會先等待可靠的模型清單,再公開模型。如果 Models 分頁中去重後的模型列達到20個,所有模型開關初始為 OFF,但供應商本身保持 ACTIVE。實際驗證方式為 OAuth 或 ChatGPT 登入的連線保留預設值。

只在首次註冊供應商時套用;更新、重新登入與更換金鑰不會重設既有選擇。初始化後,可在 Models 或使用以下 CLI 指令啟用所需模型。後續新增模型的獨立政策不變。請將 `<model-id>` 換成清單中的 ID。

```sh
ocx models live --provider openrouter
ocx models enable 'openrouter/<model-id>'
ocx models disable 'openrouter/<model-id>'
ocx models provider openrouter on
```

## 供應商相關的頂層欄位

| 欄位 | 型別 | 預設值 | 意義 |
Expand Down
1 change: 1 addition & 0 deletions gui/src/hooks/useJsonConfigEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const PROVIDER_EDITOR_DERIVED_FIELDS = [
"hasApiKey",
"hasHeaders",
"xaiResponsesOptInState",
"initialModelSelection",
] as const;

type ProviderEditorConfig = {
Expand Down
1 change: 1 addition & 0 deletions gui/tests/use-json-config-editor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const config: Config = {
allowPrivateNetwork: true,
hasApiKey: true,
hasHeaders: true,
initialModelSelection: { version: 1, registrationId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", status: "pending" },
note: "derived registry note",
},
beta: {
Expand Down
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,7 @@
"native-profile-startup.test.ts": "codex-integration",
"native-profile-store.test.ts": "codex-integration",
"new-model-policy.test.ts": "providers",
"initial-model-selection.test.ts": "providers",
"nous-oauth-live.test.ts": "providers",
"nous-oauth.test.ts": "providers",
"novita-provider.test.ts": "providers",
Expand Down
2 changes: 2 additions & 0 deletions src/cli/init.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as readline from "node:readline";
import { initializeProviderModelSelection } from "../providers/initial-model-selection";
import { existsSync, readFileSync, unlinkSync } from "node:fs";
import { injectCodexConfig } from "../codex/inject";
import { classifyOpenAiTierBackup, getConfigPath, getDefaultConfig, isValidProviderName, preserveOpenAiTierRollbackSnapshot, saveConfig } from "../config";
Expand Down Expand Up @@ -160,6 +161,7 @@ export async function runInit(): Promise<void> {
const portStr = await prompt.ask("\nProxy port [10100]: ");
const port = parseInt(portStr, 10) || 10100;

initializeProviderModelSelection(providerName, providerConfig);
const config: OcxConfig = {
...getDefaultConfig(),
port,
Expand Down
2 changes: 2 additions & 0 deletions src/cli/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ async function handleAdd(args: string[]): Promise<void> {
}

const existingProvider = config.providers[name];
const { initializeProviderModelSelection } = await import("../providers/initial-model-selection");
initializeProviderModelSelection(name, provConfig, existingProvider, config);
config.providers[name] = provConfig;
// A --force overwrite rotates the key/endpoint but must not drop a
// user-configured price overlay (same rule as the /api/providers path and
Expand Down
2 changes: 2 additions & 0 deletions src/codex/catalog/provider-fetch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
import { initialModelSelectionPending } from "../../providers/initial-model-selection";
import { execFileSync } from "node:child_process";
import { createHash, createHmac, randomBytes } from "node:crypto";
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
Expand Down Expand Up @@ -2016,6 +2017,7 @@ export function filterCatalogVisibleModels(
}
}
return models.filter(m => {
if (initialModelSelectionPending(config.providers[m.provider])) return false;
const nativeAlias = m.provider === COMBO_NAMESPACE && m.nativeAlias === true;
// disabledModels may be stored raw (canonical) or encoded (legacy UI writes).
for (const stored of disabled) {
Expand Down
10 changes: 10 additions & 0 deletions src/codex/catalog/sync.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { effectiveProviderAlias } from "../../providers/default-aliases";
import { pendingModelSelectionProviders } from "../../providers/initial-model-selection";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
Expand Down Expand Up @@ -775,6 +776,7 @@ export interface ObservedCatalogMergeInput {
readonly disabledModels: ReadonlySet<string>;
readonly selectedModelsByProvider: ReadonlyMap<string, ReadonlySet<string>>;
readonly gatheredProviderNames: ReadonlySet<string>;
readonly pendingProviderNames?: ReadonlySet<string>;
readonly degradedProviderNames: ReadonlySet<string>;
readonly legacyCustomModelSlugs: ReadonlySet<string>;
readonly multiAgentMode: MultiAgentMode;
Expand Down Expand Up @@ -806,6 +808,7 @@ export function mergeCatalogEntriesFromObservedState({
disabledModels,
selectedModelsByProvider,
gatheredProviderNames,
pendingProviderNames = new Set(),
degradedProviderNames,
legacyCustomModelSlugs,
multiAgentMode,
Expand Down Expand Up @@ -855,6 +858,7 @@ export function mergeCatalogEntriesFromObservedState({
if (disabledModelKeys.has(key)) return false;
const slash = slug.indexOf("/");
const provider = slug.slice(0, slash);
if (pendingProviderNames.has(provider)) return false;
const selected = selectedModelKeysByProvider.get(provider);
if (selected !== undefined && !selected.has(key)) return false;
return !gatheredProviderNames.has(provider) || degradedProviderNames.has(provider);
Expand Down Expand Up @@ -1050,6 +1054,7 @@ export function mergeCatalogEntriesFromObservedState({
if (freshExactComboEntries.has(entry)) return true;
const slash = slug.indexOf("/");
const provider = slug.slice(0, slash);
if (pendingProviderNames.has(provider)) return false;
const selected = selectedModelKeysByProvider.get(provider);
return selected === undefined || selected.has(slugEquivalenceKey(slug));
});
Expand Down Expand Up @@ -1718,6 +1723,7 @@ function writeRetainedCatalogSync({
disabledModels: new Set(config.disabledModels ?? []),
selectedModelsByProvider,
gatheredProviderNames,
pendingProviderNames: pendingModelSelectionProviders(config),
degradedProviderNames,
legacyCustomModelSlugs: legacyCustomModelCatalogSlugs(config),
multiAgentMode,
Expand Down Expand Up @@ -1820,6 +1826,10 @@ export async function syncCatalogModels(
config: OcxConfig,
options?: CodexCatalogSyncOptions,
): Promise<RetainedCatalogSyncResult> {
if (pendingModelSelectionProviders(config).size) {
const { resolvePendingInitialModelSelection } = await import("../../providers/initial-model-selection-runtime");
await resolvePendingInitialModelSelection(config);
}
const owningCodexHome = getCodexHome();
const preflightRead = readRetainedCatalogSync(config);
if (preflightRead === null) {
Expand Down
2 changes: 2 additions & 0 deletions src/codex/convergence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { join } from "node:path";

import { getConfigDir, saveConfigPreservingClaudeCode, websocketsEnabled, withExpectedConfigGenerationSync } from "../config";
import { reconcileSuccessfulModelDiscoveries } from "../providers/new-model-policy";
import { pendingModelSelectionProviders } from "../providers/initial-model-selection";
import { COMBO_NAMESPACE } from "../combos";
import { getAuthStorePath } from "../oauth/store";
import type { OcxConfig } from "../types";
Expand Down Expand Up @@ -351,6 +352,7 @@ function prepareCatalog(
disabledModels: new Set(config.disabledModels ?? []),
selectedModelsByProvider,
gatheredProviderNames,
pendingProviderNames: pendingModelSelectionProviders(config),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
degradedProviderNames,
legacyCustomModelSlugs: legacyCustomModelCatalogSlugs(config),
multiAgentMode,
Expand Down
3 changes: 3 additions & 0 deletions src/codex/management-convergence.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { OcxConfig } from "../types";
import { resolvePendingInitialModelSelection } from "../providers/initial-model-selection-runtime";
import { captureCatalogAdmissionSnapshot } from "./catalog-admission";
import { convergeCodexCatalog } from "./convergence";
import type {
Expand Down Expand Up @@ -152,6 +153,8 @@ export function createManagementConvergeCodex(
catalogRefresh: unexpectedCatalogFailure(false),
});
}
// Registration choices are committed independently, before sealing catalog authority.
await resolvePendingInitialModelSelection(retainedConfig as OcxConfig);
const snapshot = captureCatalogAdmissionSnapshot(retainedConfig);
const result = await convergeCodexCatalog(snapshot, request, {
onCommitBegin: () => { commitBegan = true; },
Expand Down
Loading
Loading