@openhoo/hoopilot 2.1.0 → 2.1.2
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/README.md +3 -1
- package/dist/{chunk-6ALEIJJM.js → chunk-CYR6I4C3.js} +13 -2
- package/dist/chunk-CYR6I4C3.js.map +1 -0
- package/dist/cli.js +252 -34
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +1 -1
- package/dist/index.js +219 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-6ALEIJJM.js.map +0 -1
package/README.md
CHANGED
|
@@ -248,7 +248,9 @@ npx --package @openhoo/hoopilot codexx
|
|
|
248
248
|
|
|
249
249
|
With the [keyless Docker quick start](#keyless-local-quick-start), no key is involved: `codexx --yolo` works from any directory because `codexx` sends a throwaway key that the loopback-only proxy accepts. If the server *does* require an API key, set `HOOPILOT_API_KEY` (or `CODEXX_API_KEY`) in the `codexx` environment to match.
|
|
250
250
|
|
|
251
|
-
`codexx` does not start Hoopilot and does not alter your shell environment. It starts `codex` with a temporary `hoopilot` model provider pointed at `http://127.0.0.1:4141/v1`, uses the Responses API wire format, disables Responses WebSockets for that provider, maps `HOOPILOT_API_KEY` (or a random throwaway key when none is set) to `OPENAI_API_KEY` for the child process,
|
|
251
|
+
`codexx` does not start Hoopilot and does not alter your shell environment. It starts `codex` with a temporary `hoopilot` model provider pointed at `http://127.0.0.1:4141/v1`, uses the Responses API wire format, disables Responses WebSockets for that provider, maps `HOOPILOT_API_KEY` (or a random throwaway key when none is set) to `OPENAI_API_KEY` for the child process, disables Codex's managed network proxy (`-c permissions.workspace.network.enabled=false`), and removes standard proxy variables from the spawned Codex process.
|
|
252
|
+
|
|
253
|
+
> **Why the network proxy is disabled.** Codex ships a managed network proxy that routes the agent's traffic through a local proxy (port `3128`) and enforces a domain allowlist. Because the local Hoopilot server isn't on that allowlist, leaving the proxy on makes Codex's request to Hoopilot fail with an instant `403` Squid error. The proxy has two independent gates — the `network_proxy` feature flag and the `permissions.workspace.network.enabled` config — and disabling only the feature is not always enough, so `codexx` turns off both (`--disable network_proxy` and `-c permissions.workspace.network.enabled=false`) and requests reach Hoopilot directly.
|
|
252
254
|
|
|
253
255
|
`codexx` defaults to `gpt-5.5` with `model_reasoning_effort="xhigh"`. Before starting Codex, it checks `/v1/models` and reports if the logged-in Copilot account does not advertise the requested model. Set `CODEXX_MODEL` to one of the listed models, or log in with a Copilot account that has access to the default model.
|
|
254
256
|
|
|
@@ -141,9 +141,20 @@ function buildCodexxInvocation(argv, env = process.env) {
|
|
|
141
141
|
].join(", ");
|
|
142
142
|
return {
|
|
143
143
|
args: [
|
|
144
|
+
// Codex ships a managed network proxy (codex-rs/network-proxy) that routes the
|
|
145
|
+
// agent's traffic through a local proxy on :3128 and enforces a domain allowlist.
|
|
146
|
+
// A host that is not allowlisted — like the local Hoopilot server — gets an instant
|
|
147
|
+
// 403 (the Squid error page) and never reaches Hoopilot. It has two independent
|
|
148
|
+
// gates: the `network_proxy` feature flag and the `permissions.workspace.network`
|
|
149
|
+
// config. Disabling only the feature (`--disable network_proxy`) does not reliably
|
|
150
|
+
// turn it off when the proxy is enabled through the permissions config, so set both
|
|
151
|
+
// off: the feature flag via `--disable`, and the proxy itself via the config key
|
|
152
|
+
// (when `enabled` is false the proxy no-ops and binds no listeners).
|
|
144
153
|
"--disable",
|
|
145
154
|
"network_proxy",
|
|
146
155
|
"-c",
|
|
156
|
+
"permissions.workspace.network.enabled=false",
|
|
157
|
+
"-c",
|
|
147
158
|
'model_provider="hoopilot"',
|
|
148
159
|
"-c",
|
|
149
160
|
`model_providers.hoopilot=${providerConfig}`,
|
|
@@ -253,7 +264,7 @@ Environment:
|
|
|
253
264
|
CODEXX_SKIP_MODEL_PREFLIGHT
|
|
254
265
|
Set to 1 to skip checking /v1/models before starting Codex.
|
|
255
266
|
|
|
256
|
-
codexx does not start Hoopilot and does not change your shell environment. It selects a temporary Hoopilot model provider with Responses WebSockets disabled, uses ${DEFAULT_MODEL} with ${DEFAULT_REASONING_EFFORT} reasoning by default, disables Codex's
|
|
267
|
+
codexx does not start Hoopilot and does not change your shell environment. It selects a temporary Hoopilot model provider with Responses WebSockets disabled, uses ${DEFAULT_MODEL} with ${DEFAULT_REASONING_EFFORT} reasoning by default, disables Codex's managed network proxy (permissions.workspace.network.enabled=false) so requests reach the local server instead of being blocked by its allowlist, and removes proxy variables only from the spawned Codex process.`;
|
|
257
268
|
}
|
|
258
269
|
function signalNumber(signal) {
|
|
259
270
|
return osConstants.signals[signal] ?? 1;
|
|
@@ -284,4 +295,4 @@ export {
|
|
|
284
295
|
main,
|
|
285
296
|
verifyCodexxModel
|
|
286
297
|
};
|
|
287
|
-
//# sourceMappingURL=chunk-
|
|
298
|
+
//# sourceMappingURL=chunk-CYR6I4C3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/codexx.ts","../src/util.ts"],"sourcesContent":["#!/usr/bin/env bun\n\nimport { spawn } from \"node:child_process\";\nimport { constants as osConstants } from \"node:os\";\nimport type { FetchLike } from \"./types\";\nimport {\n envValue,\n errorMessage,\n modelIdsFromResponse,\n trimTrailingSlash,\n truncatedResponseText,\n} from \"./util\";\n\nconst DEFAULT_BASE_URL = \"http://127.0.0.1:4141/v1\";\nconst DEFAULT_CODEX_BIN = \"codex\";\nconst DEFAULT_MODEL = \"gpt-5.5\";\nconst DEFAULT_REASONING_EFFORT = \"xhigh\";\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 baseUrl: string;\n command: string;\n env: NodeJS.ProcessEnv;\n model: string;\n}\n\nexport function buildCodexxInvocation(\n argv: string[],\n env: NodeJS.ProcessEnv = process.env,\n): CodexxInvocation {\n const baseUrl = envValue(env.CODEXX_BASE_URL) ?? DEFAULT_BASE_URL;\n // Never fall back to a public, predictable key: a shared constant like the old\n // \"local-key\" default is also a credential a malicious local/browser client\n // could guess. When no key is configured the local server is expected to run\n // unauthenticated, which accepts any value, so a random throwaway key is safe.\n const apiKey =\n envValue(env.CODEXX_API_KEY) ?? envValue(env.HOOPILOT_API_KEY) ?? generateEphemeralApiKey();\n const command = envValue(env.CODEXX_CODEX_BIN) ?? DEFAULT_CODEX_BIN;\n const model = envValue(env.CODEXX_MODEL) ?? DEFAULT_MODEL;\n const reasoningEffort = envValue(env.CODEXX_MODEL_REASONING_EFFORT) ?? DEFAULT_REASONING_EFFORT;\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 // Codex ships a managed network proxy (codex-rs/network-proxy) that routes the\n // agent's traffic through a local proxy on :3128 and enforces a domain allowlist.\n // A host that is not allowlisted — like the local Hoopilot server — gets an instant\n // 403 (the Squid error page) and never reaches Hoopilot. It has two independent\n // gates: the `network_proxy` feature flag and the `permissions.workspace.network`\n // config. Disabling only the feature (`--disable network_proxy`) does not reliably\n // turn it off when the proxy is enabled through the permissions config, so set both\n // off: the feature flag via `--disable`, and the proxy itself via the config key\n // (when `enabled` is false the proxy no-ops and binds no listeners).\n \"--disable\",\n \"network_proxy\",\n \"-c\",\n \"permissions.workspace.network.enabled=false\",\n \"-c\",\n 'model_provider=\"hoopilot\"',\n \"-c\",\n `model_providers.hoopilot=${providerConfig}`,\n \"-m\",\n model,\n \"-c\",\n `model_reasoning_effort=${JSON.stringify(reasoningEffort)}`,\n ...argv,\n ],\n baseUrl,\n command,\n env: withoutProxyEnv({\n ...env,\n OPENAI_API_KEY: apiKey,\n }),\n model,\n };\n}\n\n// A random, non-guessable placeholder key for when neither CODEXX_API_KEY nor\n// HOOPILOT_API_KEY is set. An unauthenticated local Hoopilot accepts any value;\n// a keyed server rejects it with a 401, which the model preflight surfaces.\nfunction generateEphemeralApiKey(): string {\n return `codexx-${crypto.randomUUID()}`;\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 if (env.CODEXX_SKIP_MODEL_PREFLIGHT !== \"1\") {\n await verifyCodexxModel(invocation);\n }\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\nexport async function verifyCodexxModel(\n invocation: Pick<CodexxInvocation, \"baseUrl\" | \"env\" | \"model\">,\n fetcher: FetchLike = fetch,\n): Promise<void> {\n const modelsUrl = `${trimTrailingSlash(invocation.baseUrl)}/models`;\n const apiKey = invocation.env.OPENAI_API_KEY;\n if (apiKey === undefined) {\n throw new Error(\n \"verifyCodexxModel requires invocation.env.OPENAI_API_KEY; build the invocation with buildCodexxInvocation.\",\n );\n }\n let response: Response;\n try {\n response = await fetcher(modelsUrl, {\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${apiKey}`,\n },\n method: \"GET\",\n });\n } catch (error) {\n throw new Error(\n `Could not reach Hoopilot at ${modelsUrl}. Start Hoopilot first, or set CODEXX_SKIP_MODEL_PREFLIGHT=1 to skip this check. ${errorMessage(error)}`,\n );\n }\n\n if (!response.ok) {\n throw new Error(\n `Could not verify model ${JSON.stringify(invocation.model)} because ${modelsUrl} returned ${response.status}: ${await truncatedResponseText(response)}`,\n );\n }\n\n const models = modelIdsFromResponse(await response.json().catch(() => undefined));\n if (models.length > 0 && !models.includes(invocation.model)) {\n throw new Error(\n `The logged-in Copilot account does not advertise model ${JSON.stringify(invocation.model)} at ${modelsUrl}. Available models: ${models.join(\", \")}. After upgrading Hoopilot, rerun \"hoopilot login\" to refresh the Copilot OAuth token, or set CODEXX_MODEL to one of the advertised model IDs.`,\n );\n }\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. When\n neither is set, a random throwaway key is generated for\n an unauthenticated local server.\n CODEXX_CODEX_BIN Codex executable to run. Default: ${DEFAULT_CODEX_BIN}\n CODEXX_MODEL Codex model to use. Default: ${DEFAULT_MODEL}\n CODEXX_MODEL_REASONING_EFFORT\n Codex reasoning effort. Default: ${DEFAULT_REASONING_EFFORT}\n CODEXX_SKIP_MODEL_PREFLIGHT\n Set to 1 to skip checking /v1/models before starting Codex.\n\ncodexx does not start Hoopilot and does not change your shell environment. It selects a temporary Hoopilot model provider with Responses WebSockets disabled, uses ${DEFAULT_MODEL} with ${DEFAULT_REASONING_EFFORT} reasoning by default, disables Codex's managed network proxy (permissions.workspace.network.enabled=false) so requests reach the local server instead of being blocked by its allowlist, 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(errorMessage(error));\n process.exit(1);\n });\n}\n","import type { JsonObject, StreamingProxyMode } from \"./types\";\n\n/** Remove any trailing slashes from a URL or path string. */\nexport function trimTrailingSlash(value: string): string {\n return value.replace(/\\/+$/, \"\");\n}\n\n/** Treat blank environment variables as unset while preserving nonblank values. */\nexport function envValue(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? trimmed : undefined;\n}\n\n/** True for HTTPS URLs, or HTTP only on loopback hosts used by local tests/dev. */\nexport function isHttpsOrLoopbackUrl(rawUrl: string): boolean {\n const url = parseUrl(rawUrl);\n if (!url) {\n return false;\n }\n return url.protocol === \"https:\" || isLoopbackHttpUrl(url);\n}\n\n/** Validate a base URL before sending a bearer/OAuth token to it. */\nexport function isTrustedTokenBaseUrl(\n rawUrl: string,\n allowedHttpsHosts: readonly string[],\n allowUnsafeHttps = false,\n): boolean {\n const url = parseUrl(rawUrl);\n if (!url) {\n return false;\n }\n if (url.username || url.password || url.search || url.hash) {\n return false;\n }\n if (url.pathname !== \"\" && url.pathname !== \"/\") {\n return false;\n }\n if (isLoopbackHttpUrl(url)) {\n return true;\n }\n if (url.protocol !== \"https:\") {\n return false;\n }\n const host = url.hostname.toLowerCase();\n return allowedHttpsHosts.includes(host) || allowUnsafeHttps;\n}\n\nfunction parseUrl(rawUrl: string): URL | undefined {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n return undefined;\n }\n return url;\n}\n\nconst LOOPBACK_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\"]);\n\n/** True for hostnames that always resolve to the local machine. */\nexport function isLoopbackHostname(host: string): boolean {\n return LOOPBACK_HOSTNAMES.has(host);\n}\n\nfunction isLoopbackHttpUrl(url: URL): boolean {\n return url.protocol === \"http:\" && isLoopbackHostname(url.hostname);\n}\n\n/** Read a response body as text, truncated to keep error messages bounded. */\nexport async function truncatedResponseText(response: Response, max = 500): Promise<string> {\n const text = await response.text();\n return text.slice(0, max);\n}\n\n/** Narrow an unknown value to a plain object, returning {} for arrays/primitives/null. */\nexport function asRecord(value: unknown): JsonObject {\n return value && typeof value === \"object\" && !Array.isArray(value) ? (value as JsonObject) : {};\n}\n\n/** Extract a human-readable message from an unknown thrown value. */\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Return the first finite number among the candidates, else undefined. */\nexport function firstNumber(...values: unknown[]): number | undefined {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value;\n }\n }\n return undefined;\n}\n\n/** Generate a dash-free random identifier for synthesized response/message ids. */\nexport function randomId(): string {\n return crypto.randomUUID().replaceAll(\"-\", \"\");\n}\n\n/** Drop keys whose value is undefined so they are omitted from JSON output. */\nexport function removeUndefined<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\n/** Parse JSON, returning undefined instead of throwing on malformed input. */\nexport function safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n}\n\n/** Parse JSON into a plain object, returning undefined on malformed or non-object input. */\nexport function parseJsonObject(text: string): JsonObject | undefined {\n try {\n return asRecord(JSON.parse(text));\n } catch {\n return undefined;\n }\n}\n\n/**\n * Extract de-duplicated model IDs from an OpenAI-style `/models` response (an\n * object carrying a `data` array, or a bare array of model objects).\n */\nexport function modelIdsFromResponse(body: unknown): string[] {\n const record = asRecord(body);\n const data = Array.isArray(record.data) ? record.data : Array.isArray(body) ? body : [];\n const seen = new Set<string>();\n const ids: string[] = [];\n for (const model of data) {\n const id = asRecord(model).id;\n if (typeof id !== \"string\" || id.length === 0 || seen.has(id)) {\n continue;\n }\n seen.add(id);\n ids.push(id);\n }\n return ids;\n}\n\n/** Canonical set of accepted streaming-proxy modes, kept in sync with {@link StreamingProxyMode}. */\nexport const STREAMING_PROXY_MODES = [\n \"auto\",\n \"buffer\",\n \"live\",\n] as const satisfies readonly StreamingProxyMode[];\n\n/** Validate a stream-mode string against the allowed {@link StreamingProxyMode} values. */\nexport function parseStreamingProxyMode(value: string): StreamingProxyMode {\n if ((STREAMING_PROXY_MODES as readonly string[]).includes(value)) {\n return value as StreamingProxyMode;\n }\n throw new Error(`Invalid stream mode: ${value}. Expected ${STREAMING_PROXY_MODES.join(\", \")}.`);\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
safeJsonParse,
|
|
16
16
|
trimTrailingSlash,
|
|
17
17
|
truncatedResponseText
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-CYR6I4C3.js";
|
|
19
19
|
|
|
20
20
|
// src/cli.ts
|
|
21
21
|
import { spawn } from "child_process";
|
|
@@ -641,6 +641,16 @@ import { Elysia } from "elysia";
|
|
|
641
641
|
|
|
642
642
|
// src/openai.ts
|
|
643
643
|
var DEFAULT_MODEL = "gpt-4.1";
|
|
644
|
+
var COMPACTION_SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
|
|
645
|
+
|
|
646
|
+
Include:
|
|
647
|
+
- Current progress and key decisions made
|
|
648
|
+
- Important context, constraints, or user preferences
|
|
649
|
+
- What remains to be done (clear next steps)
|
|
650
|
+
- Any critical data, examples, or references needed to continue
|
|
651
|
+
|
|
652
|
+
Be concise, structured, and focused on helping the next LLM seamlessly continue the work.`;
|
|
653
|
+
var COMPACTION_SUMMARY_PREFIX = "Another language model started to solve this problem and produced a summary of its thinking process. You also have access to the state of the tools that were used by that language model. Use this to build on the work that has already been done and avoid duplicating work. Here is the summary produced by the other language model, use the information in this summary to assist with your own analysis:";
|
|
644
654
|
var OpenAICompatibilityError = class extends Error {
|
|
645
655
|
constructor(message) {
|
|
646
656
|
super(message);
|
|
@@ -677,19 +687,111 @@ function normalizeRequestedModel(model) {
|
|
|
677
687
|
return requested || DEFAULT_MODEL;
|
|
678
688
|
}
|
|
679
689
|
function responsesCompactionResult(upstreamText, isSse) {
|
|
680
|
-
const
|
|
681
|
-
return { output };
|
|
690
|
+
const summary = compactionSummaryText(upstreamText, isSse);
|
|
691
|
+
return { output: [compactionSummaryMessageItem(summary)] };
|
|
692
|
+
}
|
|
693
|
+
function isResponsesCompactionRequest(request) {
|
|
694
|
+
return responseInputItems(request.input).some(
|
|
695
|
+
(item) => contentToText(asRecord(item).type) === "compaction_trigger"
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
function responsesCompactionRequestBody(request) {
|
|
699
|
+
return JSON.stringify(
|
|
700
|
+
removeUndefined({
|
|
701
|
+
...request,
|
|
702
|
+
input: [
|
|
703
|
+
...compactionInputItemsForCopilot(request.input),
|
|
704
|
+
{
|
|
705
|
+
content: [{ text: COMPACTION_SUMMARIZATION_PROMPT, type: "input_text" }],
|
|
706
|
+
role: "user",
|
|
707
|
+
type: "message"
|
|
708
|
+
}
|
|
709
|
+
],
|
|
710
|
+
parallel_tool_calls: false,
|
|
711
|
+
stream: false,
|
|
712
|
+
tool_choice: "none",
|
|
713
|
+
tools: []
|
|
714
|
+
})
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
function normalizeResponsesRequestForCopilotBody(request) {
|
|
718
|
+
return JSON.stringify(
|
|
719
|
+
removeUndefined({
|
|
720
|
+
...request,
|
|
721
|
+
input: normalizeCompactionInputForCopilot(request.input, { dropTrigger: false })
|
|
722
|
+
})
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
function responsesRequestNeedsCopilotNormalization(request) {
|
|
726
|
+
return responseInputItems(request.input).some((item) => {
|
|
727
|
+
const type = contentToText(asRecord(item).type);
|
|
728
|
+
return type === "compaction" || type === "compaction_summary" || type === "context_compaction";
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
function responsesCompactionResponse(upstreamText, isSse, model) {
|
|
732
|
+
const output = [compactionOutputItem(compactionSummaryText(upstreamText, isSse))];
|
|
733
|
+
return removeUndefined({
|
|
734
|
+
created_at: epochSeconds(),
|
|
735
|
+
error: null,
|
|
736
|
+
id: `resp_${randomId()}`,
|
|
737
|
+
incomplete_details: null,
|
|
738
|
+
instructions: null,
|
|
739
|
+
max_output_tokens: null,
|
|
740
|
+
metadata: {},
|
|
741
|
+
model,
|
|
742
|
+
object: "response",
|
|
743
|
+
output,
|
|
744
|
+
output_text: "",
|
|
745
|
+
parallel_tool_calls: false,
|
|
746
|
+
status: "completed",
|
|
747
|
+
temperature: null,
|
|
748
|
+
tool_choice: "none",
|
|
749
|
+
tools: [],
|
|
750
|
+
top_p: null
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
function responsesCompactionSseText(upstreamText, isSse, model) {
|
|
754
|
+
const responseId = `resp_${randomId()}`;
|
|
755
|
+
const item = compactionOutputItem(compactionSummaryText(upstreamText, isSse));
|
|
756
|
+
const createdAt = epochSeconds();
|
|
757
|
+
let sequenceNumber = 0;
|
|
758
|
+
const event = (name, data) => encodeSse(name, data === "[DONE]" ? data : { ...data, sequence_number: sequenceNumber++ });
|
|
759
|
+
return [
|
|
760
|
+
event("response.created", {
|
|
761
|
+
response: baseStreamResponse(responseId, model, createdAt, "in_progress", []),
|
|
762
|
+
type: "response.created"
|
|
763
|
+
}),
|
|
764
|
+
event("response.output_item.done", {
|
|
765
|
+
item,
|
|
766
|
+
output_index: 0,
|
|
767
|
+
type: "response.output_item.done"
|
|
768
|
+
}),
|
|
769
|
+
event("response.completed", {
|
|
770
|
+
response: baseStreamResponse(responseId, model, createdAt, "completed", [item]),
|
|
771
|
+
type: "response.completed"
|
|
772
|
+
}),
|
|
773
|
+
event("done", "[DONE]")
|
|
774
|
+
].join("");
|
|
682
775
|
}
|
|
683
|
-
function
|
|
684
|
-
|
|
685
|
-
|
|
776
|
+
function compactionSummaryText(upstreamText, isSse) {
|
|
777
|
+
const summary = isSse ? compactionSummaryTextFromResponsesSse(upstreamText) : compactionSummaryTextFromResponse(asRecord(safeJsonParse(upstreamText)));
|
|
778
|
+
return summary.trim() || "(no summary available)";
|
|
779
|
+
}
|
|
780
|
+
function compactionSummaryTextFromResponse(response) {
|
|
781
|
+
const output = Array.isArray(response.output) ? response.output.map((item) => asRecord(item)) : [];
|
|
782
|
+
const compaction = output.find((item) => contentToText(item.type) === "compaction");
|
|
783
|
+
if (compaction) {
|
|
784
|
+
return contentToText(compaction.encrypted_content);
|
|
785
|
+
}
|
|
786
|
+
const text = outputText(output);
|
|
787
|
+
if (text) {
|
|
788
|
+
return text;
|
|
686
789
|
}
|
|
687
|
-
|
|
688
|
-
return text ? [messageOutputItem(text)] : [];
|
|
790
|
+
return contentToText(response.output_text);
|
|
689
791
|
}
|
|
690
|
-
function
|
|
792
|
+
function compactionSummaryTextFromResponsesSse(text) {
|
|
691
793
|
let deltas = "";
|
|
692
|
-
let
|
|
794
|
+
let completedResponse;
|
|
693
795
|
for (const block of text.split(/\r?\n\r?\n/)) {
|
|
694
796
|
const data = block.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("");
|
|
695
797
|
if (!data || data === "[DONE]") {
|
|
@@ -700,16 +802,16 @@ function compactionOutputFromResponsesSse(text) {
|
|
|
700
802
|
if (type === "response.output_text.delta") {
|
|
701
803
|
deltas += contentToText(record.delta);
|
|
702
804
|
} else if (type === "response.completed" || type === "response.incomplete") {
|
|
703
|
-
|
|
704
|
-
if (Array.isArray(response.output)) {
|
|
705
|
-
completedOutput = response.output;
|
|
706
|
-
}
|
|
805
|
+
completedResponse = asRecord(record.response);
|
|
707
806
|
}
|
|
708
807
|
}
|
|
709
|
-
if (
|
|
710
|
-
|
|
808
|
+
if (completedResponse) {
|
|
809
|
+
const summary = compactionSummaryTextFromResponse(completedResponse);
|
|
810
|
+
if (summary) {
|
|
811
|
+
return summary;
|
|
812
|
+
}
|
|
711
813
|
}
|
|
712
|
-
return deltas
|
|
814
|
+
return deltas;
|
|
713
815
|
}
|
|
714
816
|
function chatCompletionToCompletion(completion) {
|
|
715
817
|
return removeUndefined({
|
|
@@ -873,21 +975,72 @@ function contentToText(content) {
|
|
|
873
975
|
}
|
|
874
976
|
return "";
|
|
875
977
|
}
|
|
876
|
-
function
|
|
978
|
+
function compactionSummaryMessageItem(text, id = `msg_${randomId()}`) {
|
|
877
979
|
return {
|
|
878
980
|
content: [
|
|
879
981
|
{
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
type: "
|
|
982
|
+
text: `${COMPACTION_SUMMARY_PREFIX}
|
|
983
|
+
${text}`,
|
|
984
|
+
type: "input_text"
|
|
883
985
|
}
|
|
884
986
|
],
|
|
885
987
|
id,
|
|
886
|
-
role: "
|
|
887
|
-
status: "completed",
|
|
988
|
+
role: "user",
|
|
888
989
|
type: "message"
|
|
889
990
|
};
|
|
890
991
|
}
|
|
992
|
+
function compactionOutputItem(text, id = `cmpct_${randomId()}`) {
|
|
993
|
+
return {
|
|
994
|
+
encrypted_content: text,
|
|
995
|
+
id,
|
|
996
|
+
type: "compaction"
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function normalizeCompactionInputForCopilot(input, options) {
|
|
1000
|
+
const items = responseInputItems(input);
|
|
1001
|
+
if (items.length === 0) {
|
|
1002
|
+
return input;
|
|
1003
|
+
}
|
|
1004
|
+
const normalized = [];
|
|
1005
|
+
for (const item of items) {
|
|
1006
|
+
const record = asRecord(item);
|
|
1007
|
+
const type = contentToText(record.type);
|
|
1008
|
+
if (type === "compaction_trigger" && options.dropTrigger) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
if (type === "compaction" || type === "compaction_summary" || type === "context_compaction") {
|
|
1012
|
+
const text = contentToText(record.encrypted_content);
|
|
1013
|
+
if (text) {
|
|
1014
|
+
normalized.push(compactionSummaryMessageItem(text));
|
|
1015
|
+
}
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
normalized.push(item);
|
|
1019
|
+
}
|
|
1020
|
+
return normalized;
|
|
1021
|
+
}
|
|
1022
|
+
function compactionInputItemsForCopilot(input) {
|
|
1023
|
+
if (Array.isArray(input)) {
|
|
1024
|
+
return normalizeCompactionInputForCopilot(input, { dropTrigger: true });
|
|
1025
|
+
}
|
|
1026
|
+
const text = contentToText(input);
|
|
1027
|
+
return text ? [
|
|
1028
|
+
{
|
|
1029
|
+
content: [{ text, type: "input_text" }],
|
|
1030
|
+
role: "user",
|
|
1031
|
+
type: "message"
|
|
1032
|
+
}
|
|
1033
|
+
] : [];
|
|
1034
|
+
}
|
|
1035
|
+
function responseInputItems(input) {
|
|
1036
|
+
return Array.isArray(input) ? input : [];
|
|
1037
|
+
}
|
|
1038
|
+
function outputText(output) {
|
|
1039
|
+
return output.flatMap((item) => {
|
|
1040
|
+
const content = item.content;
|
|
1041
|
+
return Array.isArray(content) ? content : [];
|
|
1042
|
+
}).map((part) => contentToText(asRecord(part).text)).filter(Boolean).join("");
|
|
1043
|
+
}
|
|
891
1044
|
function extractTokenUsage(usage) {
|
|
892
1045
|
const record = asRecord(usage);
|
|
893
1046
|
const prompt = firstNumber(record.prompt_tokens, record.input_tokens);
|
|
@@ -999,6 +1152,35 @@ function completionStreamError(event, parsed) {
|
|
|
999
1152
|
}
|
|
1000
1153
|
return void 0;
|
|
1001
1154
|
}
|
|
1155
|
+
function baseStreamResponse(id, model, createdAt, status, output) {
|
|
1156
|
+
return {
|
|
1157
|
+
created_at: createdAt,
|
|
1158
|
+
error: null,
|
|
1159
|
+
id,
|
|
1160
|
+
incomplete_details: null,
|
|
1161
|
+
instructions: null,
|
|
1162
|
+
max_output_tokens: null,
|
|
1163
|
+
metadata: {},
|
|
1164
|
+
model,
|
|
1165
|
+
object: "response",
|
|
1166
|
+
output,
|
|
1167
|
+
parallel_tool_calls: true,
|
|
1168
|
+
status,
|
|
1169
|
+
temperature: null,
|
|
1170
|
+
tool_choice: "auto",
|
|
1171
|
+
tools: [],
|
|
1172
|
+
top_p: null
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
function encodeSse(event, data) {
|
|
1176
|
+
if (data === "[DONE]") {
|
|
1177
|
+
return "data: [DONE]\n\n";
|
|
1178
|
+
}
|
|
1179
|
+
return `event: ${event}
|
|
1180
|
+
data: ${JSON.stringify(data)}
|
|
1181
|
+
|
|
1182
|
+
`;
|
|
1183
|
+
}
|
|
1002
1184
|
function encodeDataSse(data) {
|
|
1003
1185
|
if (data === "[DONE]") {
|
|
1004
1186
|
return "data: [DONE]\n\n";
|
|
@@ -1057,7 +1239,7 @@ function responsesStreamToAnthropicStream(stream, options) {
|
|
|
1057
1239
|
return new ReadableStream({
|
|
1058
1240
|
async start(controller) {
|
|
1059
1241
|
const enqueue = (event, data) => {
|
|
1060
|
-
controller.enqueue(encoder.encode(
|
|
1242
|
+
controller.enqueue(encoder.encode(encodeSse2(event, data)));
|
|
1061
1243
|
};
|
|
1062
1244
|
const reader = stream.getReader();
|
|
1063
1245
|
try {
|
|
@@ -1093,7 +1275,7 @@ function responsesSseTextToAnthropicSseText(text, options) {
|
|
|
1093
1275
|
const chunks = [];
|
|
1094
1276
|
const state = createAnthropicStreamState(options);
|
|
1095
1277
|
const enqueue = (event, data) => {
|
|
1096
|
-
chunks.push(
|
|
1278
|
+
chunks.push(encodeSse2(event, data));
|
|
1097
1279
|
};
|
|
1098
1280
|
for (const block of text.split(/\r?\n\r?\n/)) {
|
|
1099
1281
|
if (block.trim()) {
|
|
@@ -1361,9 +1543,9 @@ function anthropicContentFromResponsesOutput(response) {
|
|
|
1361
1543
|
}
|
|
1362
1544
|
}
|
|
1363
1545
|
if (content.length === 0) {
|
|
1364
|
-
const
|
|
1365
|
-
if (
|
|
1366
|
-
content.push({ text:
|
|
1546
|
+
const outputText2 = textValue(response.output_text);
|
|
1547
|
+
if (outputText2) {
|
|
1548
|
+
content.push({ text: outputText2, type: "text" });
|
|
1367
1549
|
}
|
|
1368
1550
|
}
|
|
1369
1551
|
return content;
|
|
@@ -1635,7 +1817,7 @@ function textValue(value) {
|
|
|
1635
1817
|
function indexValue(value) {
|
|
1636
1818
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1637
1819
|
}
|
|
1638
|
-
function
|
|
1820
|
+
function encodeSse2(event, data) {
|
|
1639
1821
|
return `event: ${event}
|
|
1640
1822
|
data: ${JSON.stringify(data)}
|
|
1641
1823
|
|
|
@@ -3516,7 +3698,21 @@ async function handleCompletions(client, metrics, recordTokens, recordExtraction
|
|
|
3516
3698
|
}
|
|
3517
3699
|
async function handleResponses(client, metrics, recordTokens, recordExtraction, request, logger, bufferProxyBodies) {
|
|
3518
3700
|
const { json, text: body } = await readJsonText(request);
|
|
3519
|
-
|
|
3701
|
+
if (isResponsesCompactionRequest(json)) {
|
|
3702
|
+
return handleResponsesCompactionV2(
|
|
3703
|
+
client,
|
|
3704
|
+
metrics,
|
|
3705
|
+
recordTokens,
|
|
3706
|
+
recordExtraction,
|
|
3707
|
+
json,
|
|
3708
|
+
request,
|
|
3709
|
+
logger
|
|
3710
|
+
);
|
|
3711
|
+
}
|
|
3712
|
+
const upstream = await client.responses(
|
|
3713
|
+
responsesRequestNeedsCopilotNormalization(json) ? normalizeResponsesRequestForCopilotBody(json) : body,
|
|
3714
|
+
request.signal
|
|
3715
|
+
);
|
|
3520
3716
|
metrics.recordUpstream("/responses", upstream.ok);
|
|
3521
3717
|
if (!upstream.ok) {
|
|
3522
3718
|
return proxyError(upstream, logger);
|
|
@@ -3536,10 +3732,7 @@ async function handleResponses(client, metrics, recordTokens, recordExtraction,
|
|
|
3536
3732
|
}
|
|
3537
3733
|
async function handleResponsesCompact(client, metrics, recordTokens, recordExtraction, request, logger) {
|
|
3538
3734
|
const body = await readJson(request);
|
|
3539
|
-
const upstream = await client.responses(
|
|
3540
|
-
JSON.stringify({ ...body, stream: false }),
|
|
3541
|
-
request.signal
|
|
3542
|
-
);
|
|
3735
|
+
const upstream = await client.responses(responsesCompactionRequestBody(body), request.signal);
|
|
3543
3736
|
metrics.recordUpstream("/responses", upstream.ok);
|
|
3544
3737
|
if (!upstream.ok) {
|
|
3545
3738
|
return proxyError(upstream, logger);
|
|
@@ -3556,6 +3749,22 @@ async function handleResponsesCompact(client, metrics, recordTokens, recordExtra
|
|
|
3556
3749
|
);
|
|
3557
3750
|
return jsonResponse(responsesCompactionResult(text, isSse));
|
|
3558
3751
|
}
|
|
3752
|
+
async function handleResponsesCompactionV2(client, metrics, recordTokens, recordExtraction, json, request, logger) {
|
|
3753
|
+
const upstream = await client.responses(responsesCompactionRequestBody(json), request.signal);
|
|
3754
|
+
metrics.recordUpstream("/responses", upstream.ok);
|
|
3755
|
+
if (!upstream.ok) {
|
|
3756
|
+
return proxyError(upstream, logger);
|
|
3757
|
+
}
|
|
3758
|
+
logUpstreamSuccess(logger, "/responses", upstream.status);
|
|
3759
|
+
const isSse = isStreamingResponse(upstream);
|
|
3760
|
+
const text = await upstream.text();
|
|
3761
|
+
const model = normalizeRequestedModel(json.model);
|
|
3762
|
+
recordResponseTextUsage(text, isSse, model, recordTokens, recordExtraction);
|
|
3763
|
+
if (json.stream === true) {
|
|
3764
|
+
return textResponse(responsesCompactionSseText(text, isSse, model), "text/event-stream");
|
|
3765
|
+
}
|
|
3766
|
+
return jsonResponse(responsesCompactionResponse(text, isSse, model));
|
|
3767
|
+
}
|
|
3559
3768
|
async function responseWithObservedUsage(response, fallbackModel, recordTokens, signal, bufferBody, recordExtraction) {
|
|
3560
3769
|
const isSse = isStreamingResponse(response);
|
|
3561
3770
|
if (bufferBody && response.body) {
|
|
@@ -3664,6 +3873,15 @@ function jsonResponse(body, status = 200) {
|
|
|
3664
3873
|
status
|
|
3665
3874
|
});
|
|
3666
3875
|
}
|
|
3876
|
+
function textResponse(body, contentType, status = 200) {
|
|
3877
|
+
return new Response(body, {
|
|
3878
|
+
headers: {
|
|
3879
|
+
...corsHeaders(),
|
|
3880
|
+
"content-type": `${contentType}; charset=utf-8`
|
|
3881
|
+
},
|
|
3882
|
+
status
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3667
3885
|
function jsonError(status, code, message) {
|
|
3668
3886
|
return jsonResponse(
|
|
3669
3887
|
{
|