File size: 4,412 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
99
100
101
102
103
104
105
106
/**
 * 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 };
  }

  // #3509: "auto" is OmniRoute's zero-config auto-routing keyword (handled by the
  // isAutoRouting path in chat.ts, not a DB combo). It must NEVER be rewritten to
  // "codex/auto" β€” ChatGPT rejects it with "The 'auto' model is not supported when using
  // Codex with a ChatGPT account". ("auto/<strategy>" already returns via the slash guard above.)
  if (requestedModel === "auto") {
    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 };
  }
}