@openhoo/hoopilot 0.5.4 → 0.5.6

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/dist/codexx.js CHANGED
@@ -20,12 +20,21 @@ function buildCodexxInvocation(argv, env = process.env) {
20
20
  const baseUrl = env.CODEXX_BASE_URL ?? DEFAULT_BASE_URL;
21
21
  const apiKey = env.CODEXX_API_KEY ?? env.HOOPILOT_API_KEY ?? env.OPENAI_API_KEY ?? DEFAULT_API_KEY;
22
22
  const command = env.CODEXX_CODEX_BIN ?? DEFAULT_CODEX_BIN;
23
+ const providerConfig = [
24
+ '{ name = "Hoopilot"',
25
+ `base_url = ${JSON.stringify(baseUrl)}`,
26
+ 'env_key = "OPENAI_API_KEY"',
27
+ 'wire_api = "responses"',
28
+ "supports_websockets = false }"
29
+ ].join(", ");
23
30
  return {
24
31
  args: [
25
32
  "--disable",
26
33
  "network_proxy",
27
34
  "-c",
28
- `openai_base_url=${JSON.stringify(baseUrl)}`,
35
+ 'model_provider="hoopilot"',
36
+ "-c",
37
+ `model_providers.hoopilot=${providerConfig}`,
29
38
  ...argv
30
39
  ],
31
40
  command,
@@ -80,7 +89,7 @@ Environment:
80
89
  OPENAI_API_KEY Used as the API key when both CODEXX_API_KEY and HOOPILOT_API_KEY are unset.
81
90
  CODEXX_CODEX_BIN Codex executable to run. Default: ${DEFAULT_CODEX_BIN}
82
91
 
83
- codexx does not start Hoopilot and does not change your shell environment. It disables Codex's network_proxy feature and removes proxy variables only from the spawned Codex process.`;
92
+ codexx does not start Hoopilot and does not change your shell environment. It selects a temporary Hoopilot model provider with Responses WebSockets disabled, disables Codex's network_proxy feature, and removes proxy variables only from the spawned Codex process.`;
84
93
  }
85
94
  function signalNumber(signal) {
86
95
  return osConstants.signals[signal] ?? 1;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/codexx.ts"],"sourcesContent":["#!/usr/bin/env bun\n\nimport { spawn } from \"node:child_process\";\nimport { constants as osConstants } from \"node:os\";\n\nconst DEFAULT_BASE_URL = \"http://127.0.0.1:4141/v1\";\nconst DEFAULT_API_KEY = \"local-key\";\nconst DEFAULT_CODEX_BIN = \"codex\";\nconst PROXY_ENV_KEYS = [\n \"ALL_PROXY\",\n \"HTTPS_PROXY\",\n \"HTTP_PROXY\",\n \"NO_PROXY\",\n \"all_proxy\",\n \"https_proxy\",\n \"http_proxy\",\n \"no_proxy\",\n];\n\nexport interface CodexxInvocation {\n args: string[];\n command: string;\n env: NodeJS.ProcessEnv;\n}\n\nexport function buildCodexxInvocation(\n argv: string[],\n env: NodeJS.ProcessEnv = process.env,\n): CodexxInvocation {\n const baseUrl = env.CODEXX_BASE_URL ?? DEFAULT_BASE_URL;\n const apiKey =\n env.CODEXX_API_KEY ?? env.HOOPILOT_API_KEY ?? env.OPENAI_API_KEY ?? DEFAULT_API_KEY;\n const command = env.CODEXX_CODEX_BIN ?? DEFAULT_CODEX_BIN;\n\n return {\n args: [\n \"--disable\",\n \"network_proxy\",\n \"-c\",\n `openai_base_url=${JSON.stringify(baseUrl)}`,\n ...argv,\n ],\n command,\n env: withoutProxyEnv({\n ...env,\n OPENAI_API_KEY: apiKey,\n }),\n };\n}\n\nfunction withoutProxyEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n const next = { ...env };\n for (const key of PROXY_ENV_KEYS) {\n delete next[key];\n }\n return next;\n}\n\nexport async function main(argv = Bun.argv.slice(2), env = process.env): Promise<void> {\n if (argv.length === 1 && (argv[0] === \"--help\" || argv[0] === \"-h\")) {\n console.log(helpText());\n return;\n }\n\n const invocation = buildCodexxInvocation(argv, env);\n const child = spawn(invocation.command, invocation.args, {\n env: invocation.env,\n shell: process.platform === \"win32\",\n stdio: \"inherit\",\n });\n\n const exitCode = await new Promise<number>((resolve, reject) => {\n child.once(\"error\", reject);\n child.once(\"exit\", (code, signal) => {\n if (typeof code === \"number\") {\n resolve(code);\n return;\n }\n resolve(signal ? 128 + signalNumber(signal) : 1);\n });\n });\n\n process.exitCode = exitCode;\n}\n\nfunction helpText(): string {\n return `codexx\n\nRun Codex against an already-running local Hoopilot server.\n\nUsage:\n codexx [codex options] [prompt]\n\nEnvironment:\n CODEXX_BASE_URL OpenAI-compatible base URL. Default: ${DEFAULT_BASE_URL}\n CODEXX_API_KEY API key sent to the local Hoopilot server.\n HOOPILOT_API_KEY Used as the API key when CODEXX_API_KEY is unset.\n OPENAI_API_KEY Used as the API key when both CODEXX_API_KEY and HOOPILOT_API_KEY are unset.\n CODEXX_CODEX_BIN Codex executable to run. Default: ${DEFAULT_CODEX_BIN}\n\ncodexx does not start Hoopilot and does not change your shell environment. It disables Codex's network_proxy feature and removes proxy variables only from the spawned Codex process.`;\n}\n\nfunction signalNumber(signal: NodeJS.Signals): number {\n return osConstants.signals[signal] ?? 1;\n}\n\nif (import.meta.main) {\n main().catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : String(error));\n process.exit(1);\n });\n}\n"],"mappings":";;;AAEA,SAAS,aAAa;AACtB,SAAS,aAAa,mBAAmB;AAEzC,IAAM,mBAAmB;AACzB,IAAM,kBAAkB;AACxB,IAAM,oBAAoB;AAC1B,IAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQO,SAAS,sBACd,MACA,MAAyB,QAAQ,KACf;AAClB,QAAM,UAAU,IAAI,mBAAmB;AACvC,QAAM,SACJ,IAAI,kBAAkB,IAAI,oBAAoB,IAAI,kBAAkB;AACtE,QAAM,UAAU,IAAI,oBAAoB;AAExC,SAAO;AAAA,IACL,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,mBAAmB,KAAK,UAAU,OAAO,CAAC;AAAA,MAC1C,GAAG;AAAA,IACL;AAAA,IACA;AAAA,IACA,KAAK,gBAAgB;AAAA,MACnB,GAAG;AAAA,MACH,gBAAgB;AAAA,IAClB,CAAC;AAAA,EACH;AACF;AAEA,SAAS,gBAAgB,KAA2C;AAClE,QAAM,OAAO,EAAE,GAAG,IAAI;AACtB,aAAW,OAAO,gBAAgB;AAChC,WAAO,KAAK,GAAG;AAAA,EACjB;AACA,SAAO;AACT;AAEA,eAAsB,KAAK,OAAO,IAAI,KAAK,MAAM,CAAC,GAAG,MAAM,QAAQ,KAAoB;AACrF,MAAI,KAAK,WAAW,MAAM,KAAK,CAAC,MAAM,YAAY,KAAK,CAAC,MAAM,OAAO;AACnE,YAAQ,IAAI,SAAS,CAAC;AACtB;AAAA,EACF;AAEA,QAAM,aAAa,sBAAsB,MAAM,GAAG;AAClD,QAAM,QAAQ,MAAM,WAAW,SAAS,WAAW,MAAM;AAAA,IACvD,KAAK,WAAW;AAAA,IAChB,OAAO,QAAQ,aAAa;AAAA,IAC5B,OAAO;AAAA,EACT,CAAC;AAED,QAAM,WAAW,MAAM,IAAI,QAAgB,CAAC,SAAS,WAAW;AAC9D,UAAM,KAAK,SAAS,MAAM;AAC1B,UAAM,KAAK,QAAQ,CAAC,MAAM,WAAW;AACnC,UAAI,OAAO,SAAS,UAAU;AAC5B,gBAAQ,IAAI;AACZ;AAAA,MACF;AACA,cAAQ,SAAS,MAAM,aAAa,MAAM,IAAI,CAAC;AAAA,IACjD,CAAC;AAAA,EACH,CAAC;AAED,UAAQ,WAAW;AACrB;AAEA,SAAS,WAAmB;AAC1B,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,8DAQqD,gBAAgB;AAAA;AAAA;AAAA;AAAA,2DAInB,iBAAiB;AAAA;AAAA;AAG5E;AAEA,SAAS,aAAa,QAAgC;AACpD,SAAO,YAAY,QAAQ,MAAM,KAAK;AACxC;AAEA,IAAI,YAAY,MAAM;AACpB,OAAK,EAAE,MAAM,CAAC,UAAmB;AAC/B,YAAQ,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AACpE,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/codexx.ts"],"sourcesContent":["#!/usr/bin/env bun\n\nimport { spawn } from \"node:child_process\";\nimport { constants as osConstants } from \"node:os\";\n\nconst DEFAULT_BASE_URL = \"http://127.0.0.1:4141/v1\";\nconst DEFAULT_API_KEY = \"local-key\";\nconst DEFAULT_CODEX_BIN = \"codex\";\nconst PROXY_ENV_KEYS = [\n \"ALL_PROXY\",\n \"HTTPS_PROXY\",\n \"HTTP_PROXY\",\n \"NO_PROXY\",\n \"all_proxy\",\n \"https_proxy\",\n \"http_proxy\",\n \"no_proxy\",\n];\n\nexport interface CodexxInvocation {\n args: string[];\n command: string;\n env: NodeJS.ProcessEnv;\n}\n\nexport function buildCodexxInvocation(\n argv: string[],\n env: NodeJS.ProcessEnv = process.env,\n): CodexxInvocation {\n const baseUrl = env.CODEXX_BASE_URL ?? DEFAULT_BASE_URL;\n const apiKey =\n env.CODEXX_API_KEY ?? env.HOOPILOT_API_KEY ?? env.OPENAI_API_KEY ?? DEFAULT_API_KEY;\n const command = env.CODEXX_CODEX_BIN ?? DEFAULT_CODEX_BIN;\n const providerConfig = [\n '{ name = \"Hoopilot\"',\n `base_url = ${JSON.stringify(baseUrl)}`,\n 'env_key = \"OPENAI_API_KEY\"',\n 'wire_api = \"responses\"',\n \"supports_websockets = false }\",\n ].join(\", \");\n\n return {\n args: [\n \"--disable\",\n \"network_proxy\",\n \"-c\",\n 'model_provider=\"hoopilot\"',\n \"-c\",\n `model_providers.hoopilot=${providerConfig}`,\n ...argv,\n ],\n command,\n env: withoutProxyEnv({\n ...env,\n OPENAI_API_KEY: apiKey,\n }),\n };\n}\n\nfunction withoutProxyEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n const next = { ...env };\n for (const key of PROXY_ENV_KEYS) {\n delete next[key];\n }\n return next;\n}\n\nexport async function main(argv = Bun.argv.slice(2), env = process.env): Promise<void> {\n if (argv.length === 1 && (argv[0] === \"--help\" || argv[0] === \"-h\")) {\n console.log(helpText());\n return;\n }\n\n const invocation = buildCodexxInvocation(argv, env);\n const child = spawn(invocation.command, invocation.args, {\n env: invocation.env,\n shell: process.platform === \"win32\",\n stdio: \"inherit\",\n });\n\n const exitCode = await new Promise<number>((resolve, reject) => {\n child.once(\"error\", reject);\n child.once(\"exit\", (code, signal) => {\n if (typeof code === \"number\") {\n resolve(code);\n return;\n }\n resolve(signal ? 128 + signalNumber(signal) : 1);\n });\n });\n\n process.exitCode = exitCode;\n}\n\nfunction helpText(): string {\n return `codexx\n\nRun Codex against an already-running local Hoopilot server.\n\nUsage:\n codexx [codex options] [prompt]\n\nEnvironment:\n CODEXX_BASE_URL OpenAI-compatible base URL. Default: ${DEFAULT_BASE_URL}\n CODEXX_API_KEY API key sent to the local Hoopilot server.\n HOOPILOT_API_KEY Used as the API key when CODEXX_API_KEY is unset.\n OPENAI_API_KEY Used as the API key when both CODEXX_API_KEY and HOOPILOT_API_KEY are unset.\n CODEXX_CODEX_BIN Codex executable to run. Default: ${DEFAULT_CODEX_BIN}\n\ncodexx does not start Hoopilot and does not change your shell environment. It selects a temporary Hoopilot model provider with Responses WebSockets disabled, disables Codex's network_proxy feature, and removes proxy variables only from the spawned Codex process.`;\n}\n\nfunction signalNumber(signal: NodeJS.Signals): number {\n return osConstants.signals[signal] ?? 1;\n}\n\nif (import.meta.main) {\n main().catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : String(error));\n process.exit(1);\n });\n}\n"],"mappings":";;;AAEA,SAAS,aAAa;AACtB,SAAS,aAAa,mBAAmB;AAEzC,IAAM,mBAAmB;AACzB,IAAM,kBAAkB;AACxB,IAAM,oBAAoB;AAC1B,IAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQO,SAAS,sBACd,MACA,MAAyB,QAAQ,KACf;AAClB,QAAM,UAAU,IAAI,mBAAmB;AACvC,QAAM,SACJ,IAAI,kBAAkB,IAAI,oBAAoB,IAAI,kBAAkB;AACtE,QAAM,UAAU,IAAI,oBAAoB;AACxC,QAAM,iBAAiB;AAAA,IACrB;AAAA,IACA,cAAc,KAAK,UAAU,OAAO,CAAC;AAAA,IACrC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AAEX,SAAO;AAAA,IACL,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,4BAA4B,cAAc;AAAA,MAC1C,GAAG;AAAA,IACL;AAAA,IACA;AAAA,IACA,KAAK,gBAAgB;AAAA,MACnB,GAAG;AAAA,MACH,gBAAgB;AAAA,IAClB,CAAC;AAAA,EACH;AACF;AAEA,SAAS,gBAAgB,KAA2C;AAClE,QAAM,OAAO,EAAE,GAAG,IAAI;AACtB,aAAW,OAAO,gBAAgB;AAChC,WAAO,KAAK,GAAG;AAAA,EACjB;AACA,SAAO;AACT;AAEA,eAAsB,KAAK,OAAO,IAAI,KAAK,MAAM,CAAC,GAAG,MAAM,QAAQ,KAAoB;AACrF,MAAI,KAAK,WAAW,MAAM,KAAK,CAAC,MAAM,YAAY,KAAK,CAAC,MAAM,OAAO;AACnE,YAAQ,IAAI,SAAS,CAAC;AACtB;AAAA,EACF;AAEA,QAAM,aAAa,sBAAsB,MAAM,GAAG;AAClD,QAAM,QAAQ,MAAM,WAAW,SAAS,WAAW,MAAM;AAAA,IACvD,KAAK,WAAW;AAAA,IAChB,OAAO,QAAQ,aAAa;AAAA,IAC5B,OAAO;AAAA,EACT,CAAC;AAED,QAAM,WAAW,MAAM,IAAI,QAAgB,CAAC,SAAS,WAAW;AAC9D,UAAM,KAAK,SAAS,MAAM;AAC1B,UAAM,KAAK,QAAQ,CAAC,MAAM,WAAW;AACnC,UAAI,OAAO,SAAS,UAAU;AAC5B,gBAAQ,IAAI;AACZ;AAAA,MACF;AACA,cAAQ,SAAS,MAAM,aAAa,MAAM,IAAI,CAAC;AAAA,IACjD,CAAC;AAAA,EACH,CAAC;AAED,UAAQ,WAAW;AACrB;AAEA,SAAS,WAAmB;AAC1B,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,8DAQqD,gBAAgB;AAAA;AAAA;AAAA;AAAA,2DAInB,iBAAiB;AAAA;AAAA;AAG5E;AAEA,SAAS,aAAa,QAAgC;AACpD,SAAO,YAAY,QAAQ,MAAM,KAAK;AACxC;AAEA,IAAI,YAAY,MAAM;AACpB,OAAK,EAAE,MAAM,CAAC,UAAmB;AAC/B,YAAQ,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AACpE,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;","names":[]}
package/dist/index.cjs CHANGED
@@ -45,7 +45,9 @@ __export(index_exports, {
45
45
  fallbackModels: () => fallbackModels,
46
46
  githubCopilotDeviceLogin: () => githubCopilotDeviceLogin,
47
47
  noopLogger: () => noopLogger,
48
+ normalizeChatCompletionRequest: () => normalizeChatCompletionRequest,
48
49
  normalizeModelsResponse: () => normalizeModelsResponse,
50
+ normalizeRequestedModel: () => normalizeRequestedModel,
49
51
  parseLogFormat: () => parseLogFormat,
50
52
  parseLogLevel: () => parseLogLevel,
51
53
  readStoredCopilotAuth: () => readStoredCopilotAuth,
@@ -172,16 +174,6 @@ var CopilotClient = class {
172
174
  signal
173
175
  });
174
176
  }
175
- async forwardChatCompletions(body, signal) {
176
- return this.fetchCopilot("/chat/completions", {
177
- body,
178
- headers: {
179
- "content-type": "application/json"
180
- },
181
- method: "POST",
182
- signal
183
- });
184
- }
185
177
  async models(signal) {
186
178
  return this.fetchCopilot("/models", {
187
179
  headers: {
@@ -431,6 +423,10 @@ function isLogLevel(value) {
431
423
 
432
424
  // src/openai.ts
433
425
  var DEFAULT_MODEL = "gpt-4.1";
426
+ var MODEL_ALIASES = {
427
+ "gpt-5.5": DEFAULT_MODEL,
428
+ "gpt-5.5-codex": DEFAULT_MODEL
429
+ };
434
430
  function responsesRequestToChatCompletion(request) {
435
431
  const messages = [];
436
432
  const instructions = contentToText(request.instructions);
@@ -445,7 +441,7 @@ function responsesRequestToChatCompletion(request) {
445
441
  max_tokens: request.max_output_tokens ?? request.max_tokens,
446
442
  messages,
447
443
  metadata: request.metadata,
448
- model: contentToText(request.model) || DEFAULT_MODEL,
444
+ model: normalizeRequestedModel(request.model),
449
445
  presence_penalty: request.presence_penalty,
450
446
  reasoning_effort: asRecord(request.reasoning).effort,
451
447
  response_format: asRecord(request.text).format,
@@ -457,16 +453,29 @@ function responsesRequestToChatCompletion(request) {
457
453
  top_p: request.top_p
458
454
  });
459
455
  }
456
+ function normalizeChatCompletionRequest(request) {
457
+ return removeUndefined({
458
+ ...request,
459
+ model: normalizeRequestedModel(request.model)
460
+ });
461
+ }
460
462
  function completionsRequestToChatCompletion(request) {
461
463
  return removeUndefined({
462
464
  max_tokens: request.max_tokens,
463
465
  messages: [{ content: promptToText(request.prompt), role: "user" }],
464
- model: contentToText(request.model) || DEFAULT_MODEL,
466
+ model: normalizeRequestedModel(request.model),
465
467
  stream: request.stream === true,
466
468
  temperature: request.temperature,
467
469
  top_p: request.top_p
468
470
  });
469
471
  }
472
+ function normalizeRequestedModel(model) {
473
+ const requested = contentToText(model).trim();
474
+ if (!requested) {
475
+ return DEFAULT_MODEL;
476
+ }
477
+ return MODEL_ALIASES[requested.toLowerCase()] ?? requested;
478
+ }
470
479
  function chatCompletionToResponse(completion, responseId) {
471
480
  const id = responseId ?? `resp_${randomId()}`;
472
481
  const choice = firstChoice(completion);
@@ -999,6 +1008,13 @@ function createHoopilotHandler(options = {}) {
999
1008
  { logger: requestLogger, requestId, startedAt }
1000
1009
  );
1001
1010
  }
1011
+ if (request.method === "GET" && apiPath === "/v1/responses") {
1012
+ return finishResponse(websocketUnsupportedResponse(), {
1013
+ logger: requestLogger,
1014
+ requestId,
1015
+ startedAt
1016
+ });
1017
+ }
1002
1018
  if (request.method === "GET" && apiPath === "/v1/models") {
1003
1019
  return finishResponse(await handleModels(client, request.signal, requestLogger), {
1004
1020
  logger: requestLogger,
@@ -1108,7 +1124,9 @@ async function handleModels(client, signal, logger) {
1108
1124
  return jsonResponse(normalizeModelsResponse(await upstream.json()));
1109
1125
  }
1110
1126
  async function handleChatCompletions(client, request, logger) {
1111
- const upstream = await client.forwardChatCompletions(await request.text(), request.signal);
1127
+ const chatRequest = normalizeChatCompletionRequest(await readJson(request));
1128
+ const result = await sendChatCompletions(client, chatRequest, request.signal, logger);
1129
+ const upstream = result.upstream;
1112
1130
  if (!upstream.ok) {
1113
1131
  return proxyError(upstream, logger);
1114
1132
  }
@@ -1117,10 +1135,13 @@ async function handleChatCompletions(client, request, logger) {
1117
1135
  }
1118
1136
  async function handleCompletions(client, request, logger) {
1119
1137
  const body = await readJson(request);
1120
- const upstream = await client.chatCompletions(
1138
+ const result = await sendChatCompletions(
1139
+ client,
1121
1140
  completionsRequestToChatCompletion(body),
1122
- request.signal
1141
+ request.signal,
1142
+ logger
1123
1143
  );
1144
+ const upstream = result.upstream;
1124
1145
  if (!upstream.ok) {
1125
1146
  return proxyError(upstream, logger);
1126
1147
  }
@@ -1130,7 +1151,8 @@ async function handleCompletions(client, request, logger) {
1130
1151
  async function handleResponses(client, request, logger) {
1131
1152
  const body = await readJson(request);
1132
1153
  const chatRequest = responsesRequestToChatCompletion(body);
1133
- const upstream = await client.chatCompletions(chatRequest, request.signal);
1154
+ const result = await sendChatCompletions(client, chatRequest, request.signal, logger);
1155
+ const upstream = result.upstream;
1134
1156
  if (!upstream.ok) {
1135
1157
  return proxyError(upstream, logger);
1136
1158
  }
@@ -1138,7 +1160,7 @@ async function handleResponses(client, request, logger) {
1138
1160
  if (body.stream === true && upstream.body) {
1139
1161
  return new Response(
1140
1162
  responsesStreamFromChatStream(upstream.body, {
1141
- model: typeof chatRequest.model === "string" ? chatRequest.model : "gpt-4.1"
1163
+ model: result.model
1142
1164
  }),
1143
1165
  {
1144
1166
  headers: {
@@ -1152,6 +1174,52 @@ async function handleResponses(client, request, logger) {
1152
1174
  }
1153
1175
  return jsonResponse(chatCompletionToResponse(await upstream.json()));
1154
1176
  }
1177
+ async function sendChatCompletions(client, chatRequest, signal, logger) {
1178
+ const model = requestModel(chatRequest);
1179
+ const upstream = await client.chatCompletions(chatRequest, signal);
1180
+ if (upstream.ok || isUpstreamAuthStatus(upstream.status)) {
1181
+ return { model, upstream };
1182
+ }
1183
+ const text = await upstream.text();
1184
+ if (!shouldRetryWithDefaultModel(upstream.status, text, model)) {
1185
+ return { model, upstream: textResponse(upstream, text) };
1186
+ }
1187
+ logger.warn(
1188
+ {
1189
+ event: "copilot.model.fallback",
1190
+ fallbackModel: DEFAULT_MODEL,
1191
+ upstreamPath: "/chat/completions",
1192
+ upstreamStatus: upstream.status
1193
+ },
1194
+ "retrying chat completion with fallback model"
1195
+ );
1196
+ return {
1197
+ model: DEFAULT_MODEL,
1198
+ upstream: await client.chatCompletions({ ...chatRequest, model: DEFAULT_MODEL }, signal)
1199
+ };
1200
+ }
1201
+ function shouldRetryWithDefaultModel(status, text, model) {
1202
+ if (model === DEFAULT_MODEL || status < 400 || status >= 500) {
1203
+ return false;
1204
+ }
1205
+ const normalized = text.toLowerCase();
1206
+ return normalized.includes("model") && (normalized.includes("not supported") || normalized.includes("unsupported") || normalized.includes("invalid model"));
1207
+ }
1208
+ function requestModel(request) {
1209
+ return typeof request.model === "string" && request.model.trim() ? request.model : DEFAULT_MODEL;
1210
+ }
1211
+ function textResponse(upstream, text) {
1212
+ const headers = new Headers();
1213
+ const contentType = upstream.headers.get("content-type");
1214
+ if (contentType) {
1215
+ headers.set("content-type", contentType);
1216
+ }
1217
+ return new Response(text, {
1218
+ headers,
1219
+ status: upstream.status,
1220
+ statusText: upstream.statusText
1221
+ });
1222
+ }
1155
1223
  async function proxyError(upstream, logger) {
1156
1224
  const text = await upstream.text();
1157
1225
  if (isUpstreamAuthStatus(upstream.status)) {
@@ -1210,6 +1278,15 @@ function jsonError(status, code, message) {
1210
1278
  status
1211
1279
  );
1212
1280
  }
1281
+ function websocketUnsupportedResponse() {
1282
+ const response = jsonError(
1283
+ 426,
1284
+ "websocket_not_supported",
1285
+ "Hoopilot does not support Responses WebSocket transport; retry with HTTP Responses API."
1286
+ );
1287
+ response.headers.set("upgrade", "websocket");
1288
+ return response;
1289
+ }
1213
1290
  function corsHeaders() {
1214
1291
  return {
1215
1292
  "access-control-allow-headers": "authorization, content-type, x-api-key",
@@ -1319,6 +1396,9 @@ function routeFor(method, path) {
1319
1396
  if (method === "POST" && path === "/v1/responses") {
1320
1397
  return "responses";
1321
1398
  }
1399
+ if (method === "GET" && path === "/v1/responses") {
1400
+ return "responses_websocket";
1401
+ }
1322
1402
  return "not_found";
1323
1403
  }
1324
1404
  function isStreamingResponse(response) {
@@ -1361,7 +1441,9 @@ function errorDetails(error) {
1361
1441
  fallbackModels,
1362
1442
  githubCopilotDeviceLogin,
1363
1443
  noopLogger,
1444
+ normalizeChatCompletionRequest,
1364
1445
  normalizeModelsResponse,
1446
+ normalizeRequestedModel,
1365
1447
  parseLogFormat,
1366
1448
  parseLogLevel,
1367
1449
  readStoredCopilotAuth,