File size: 4,058 Bytes
6111b2b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**

 * Model resolution for the Codex Responses-over-WebSocket bridge.

 *

 * The bridge is codex-only, but the OpenAI Codex CLI rejects provider-prefixed

 * model ids (e.g. "codex/gpt-5.5") client-side when `supports_websockets` is

 * enabled β€” it only accepts bare ChatGPT model ids (e.g. "gpt-5.5"). Those bare

 * ids can resolve to a different default provider (openai / openrouter) under

 * OmniRoute's global model routing, which the bridge would then reject with

 * `codex_ws_provider_required` (or fail the credentials lookup).

 *

 * Since this endpoint only ever talks to the Codex upstream, re-resolve a bare

 * id under the `codex/` prefix so it is treated as codex. Provider-prefixed ids

 * (already containing a "/") are left untouched.

 *

 * See docs/reference/API_REFERENCE.md β†’ "Responses over WebSocket (Codex)".

 */

export interface ResolvedModelInfo {
  provider?: string;
  model?: string;
  [key: string]: unknown;
}

export type ModelResolver = (modelStr: string) => Promise<ResolvedModelInfo>;

/**

 * Resolve a Responses-WebSocket model id, preferring the codex provider.

 *

 * @param requestedModel the bare/prefixed model id sent by the client

 * @param resolve a `getModelInfo`-style resolver

 * @returns the codex-preferred resolution, or the original resolution if the

 *          model genuinely does not map to codex.

 */
export async function resolveCodexWsModelInfo(

  requestedModel: string,

  resolve: ModelResolver

): Promise<ResolvedModelInfo> {
  const info = await resolve(requestedModel);

  // Already codex, or explicitly provider-prefixed β†’ respect it.
  if (info?.provider === "codex" || requestedModel.includes("/")) {
    return info;
  }

  // Bare id resolved to a non-codex provider; retry as a codex model.
  const codexInfo = await resolve(`codex/${requestedModel}`);
  return codexInfo?.provider === "codex" ? codexInfo : info;
}

/**

 * Resolve a model ID for the HTTP Responses path, applying codex preference

 * for bare ChatGPT-style model IDs (those without a provider prefix).

 *

 * When the Codex CLI falls back from WebSocket to HTTP (#15492), it sends bare

 * model IDs like "gpt-5.5" to /v1/responses. Without this resolution, OmniRoute

 * routes them to openrouter/openai instead of the configured codex OAuth

 * connections, producing "No credentials for provider: openrouter".

 *

 * @param requestedModel the model id from the Responses API request body

 * @param resolve a getModelInfo-style resolver

 * @param isCombo optional predicate β€” when the bare id is a combo name, skip the codex

 *        rewrite so downstream combo routing resolves it (#3227/#3233).

 * @returns { model, changed } β€” model is the (possibly rewritten) id;

 *          changed=true means a codex/ prefix was applied.

 */
export async function resolveResponsesApiModel(

  requestedModel: string,

  resolve: ModelResolver,

  isCombo?: (name: string) => Promise<boolean> | boolean

): Promise<{ model: string; changed: boolean }> {
  if (!requestedModel || requestedModel.includes("/")) {
    return { model: requestedModel, changed: false };
  }

  // #3227/#3233: a bare combo name (e.g. "n8n-text", "paid-premium") must NOT be
  // force-prefixed to codex/ β€” Codex accepts arbitrary model strings, so the rewrite
  // would shadow the combo and route to codex. Let downstream combo routing handle it.
  if (isCombo) {
    try {
      if (await isCombo(requestedModel)) return { model: requestedModel, changed: false };
    } catch {
      // combo lookup unavailable β€” fall through to normal codex-preference resolution
    }
  }

  try {
    const resolved = await resolveCodexWsModelInfo(requestedModel, resolve);
    if (resolved?.provider !== "codex") {
      return { model: requestedModel, changed: false };
    }

    const prefixed = `codex/${resolved.model || requestedModel}`;
    return { model: prefixed, changed: true };
  } catch {
    return { model: requestedModel, changed: false };
  }
}