ghc-proxy 0.7.0 → 0.7.1

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 CHANGED
@@ -195,6 +195,8 @@ All fields are optional. The full schema:
195
195
  | `responsesApiAutoCompactInput` | `boolean` | `false` | Automatically trim Responses `input` to the latest `compaction` item |
196
196
  | `responsesApiAutoContextManagement` | `boolean` | `false` | Automatically inject Responses `context_management` for selected models |
197
197
  | `responsesApiContextManagementModels` | `string[]` | -- | Models eligible for auto-injected Responses `context_management` |
198
+ | `responsesApiParameterFilters` | `{ models, params }[]` | -- | Extra rules to strip request parameters on the Responses boundary (see [Responses Parameter Filters](#responses-parameter-filters)) |
199
+ | `responsesApiParameterFiltersReplaceDefault` | `boolean` | `false` | Disable the built-in reasoning-model default rule so only your `responsesApiParameterFilters` apply |
198
200
  | `responsesOfficialEmulator` | `boolean` | `false` | Enable local OpenAI-style Responses state emulation for `previous_response_id`, `conversation`, retrieve, input_items, delete, and input_tokens |
199
201
  | `responsesOfficialEmulatorTtlSeconds` | `number` | `14400` | In-memory TTL for locally emulated Responses state |
200
202
  | `modelReasoningEfforts` | `Record<string, string>` | -- | Per-model reasoning effort defaults for Anthropic-to-Responses translation |
@@ -381,6 +383,7 @@ This keeps the existing chat pipeline stable while allowing newer Copilot models
381
383
  - automatic Responses `context_management` injection is disabled by default and only applies when `responsesApiAutoContextManagement` is `true` and the model matches `responsesApiContextManagementModels`
382
384
  - automatic trimming of Responses `input` to the latest `compaction` item is disabled by default and only applies when `responsesApiAutoCompactInput` is `true`
383
385
  - reasoning defaults for Anthropic -> Responses translation can be tuned with `modelReasoningEfforts`
386
+ - request parameters that a model rejects (e.g. `temperature`/`top_p` on reasoning models) are stripped on the Responses boundary rather than leaked upstream as a `400`; see [Responses Parameter Filters](#responses-parameter-filters)
384
387
  - known unsupported builtin tools, such as `web_search`, fail explicitly with `400` instead of being silently removed
385
388
  - external image URLs on the Responses path fail explicitly with `400`; use `file_id` or data URL image input instead
386
389
  - official `input_file` and `item_reference` input items are modeled explicitly and validated before forwarding
@@ -399,6 +402,28 @@ Example opt-in configuration for these two Responses-specific policies:
399
402
 
400
403
  > See [Responses Upstream Notes](./docs/responses-upstream-notes.md) for detailed upstream compatibility observations from live testing.
401
404
 
405
+ ### Responses Parameter Filters
406
+
407
+ Some Copilot models reject request parameters that the OpenAI wire format allows. The clearest case: **reasoning models** (the `gpt-5` family, o-series, codex) reject sampling parameters and answer `POST /responses` with `400 Unsupported parameter: 'temperature' is not supported with this model.` Since the client cannot always be changed, the proxy strips the offending parameters on the Responses boundary instead of leaking the incompatibility outward.
408
+
409
+ This is expressed as a small rule engine that runs on both the native `/v1/responses` path and the `/v1/messages` → Responses translation path:
410
+
411
+ - **Built-in default rule:** any model that advertises `reasoning_effort` has `temperature` and `top_p` stripped. This covers the whole reasoning family (including future point releases like `gpt-5.4-mini`) with no configuration.
412
+ - **`responsesApiParameterFilters`:** add your own rules. Each rule is `{ "models": [glob, ...], "params": [name, ...] }`; every rule whose `models` glob matches the resolved model contributes its `params`. Rules are **added** to the default (the union of parameters is stripped). Model globs use the same `*` wildcard as `modelRewrites`.
413
+ - **`responsesApiParameterFiltersReplaceDefault`:** set to `true` to disable the built-in reasoning-model rule, so only your `responsesApiParameterFilters` apply — use this to fully **overwrite** the default behavior.
414
+
415
+ Stripped parameters are removed entirely (never sent as `null`), because upstream rejects the mere presence of the key.
416
+
417
+ ```json
418
+ {
419
+ "responsesApiParameterFilters": [
420
+ { "models": ["gpt-5*", "o1*"], "params": ["temperature", "top_p"] },
421
+ { "models": ["some-model"], "params": ["top_k"] }
422
+ ],
423
+ "responsesApiParameterFiltersReplaceDefault": false
424
+ }
425
+ ```
426
+
402
427
  ## Docker
403
428
 
404
429
  Pre-built images are available on GHCR:
package/dist/main.mjs CHANGED
@@ -5754,6 +5754,11 @@ const configFileSchema = object({
5754
5754
  responsesApiAutoCompactInput: boolean().optional(),
5755
5755
  responsesApiAutoContextManagement: boolean().optional(),
5756
5756
  responsesApiContextManagementModels: array(string()).optional(),
5757
+ responsesApiParameterFilters: array(object({
5758
+ models: array(string()).min(1),
5759
+ params: array(string()).min(1)
5760
+ })).optional(),
5761
+ responsesApiParameterFiltersReplaceDefault: boolean().optional(),
5757
5762
  responsesOfficialEmulator: boolean().optional(),
5758
5763
  responsesOfficialEmulatorTtlSeconds: number().int().positive().optional(),
5759
5764
  modelReasoningEfforts: record(string(), reasoningEffortSchema).optional(),
@@ -5871,6 +5876,12 @@ var ConfigStore = class {
5871
5876
  getReasoningEffort(model) {
5872
5877
  return getCachedConfig().modelReasoningEfforts?.[model] ?? "high";
5873
5878
  }
5879
+ getResponsesParameterFilters() {
5880
+ return getCachedConfig().responsesApiParameterFilters ?? [];
5881
+ }
5882
+ shouldReplaceDefaultParameterFilters() {
5883
+ return getCachedConfig().responsesApiParameterFiltersReplaceDefault ?? false;
5884
+ }
5874
5885
  getModelRewrites() {
5875
5886
  return getCachedConfig().modelRewrites ?? [];
5876
5887
  }
@@ -5933,6 +5944,9 @@ var ModelCache = class {
5933
5944
  supportsVision(model) {
5934
5945
  return model?.capabilities.supports.vision ?? false;
5935
5946
  }
5947
+ supportsReasoningEffort(model) {
5948
+ return (model?.capabilities.supports.reasoning_effort?.length ?? 0) > 0;
5949
+ }
5936
5950
  supportsOutputConfig(model) {
5937
5951
  if (!model) return true;
5938
5952
  return !MODELS_REJECTING_OUTPUT_CONFIG.has(model.id);
@@ -7350,7 +7364,7 @@ const checkUsage = defineCommand({
7350
7364
  });
7351
7365
  //#endregion
7352
7366
  //#region src/util/version.ts
7353
- const VERSION = "0.7.0";
7367
+ const VERSION = "0.7.1";
7354
7368
  //#endregion
7355
7369
  //#region src/debug.ts
7356
7370
  function getRuntimeInfo() {
@@ -50865,6 +50879,61 @@ function containsVisionContent(value) {
50865
50879
  return false;
50866
50880
  }
50867
50881
  //#endregion
50882
+ //#region src/transform/parameter-filter.ts
50883
+ /**
50884
+ * Parameters the default rule strips for reasoning models on the Responses
50885
+ * boundary. Reasoning models (gpt-5 family, o-series, codex) reject sampling
50886
+ * parameters upstream with a 400 "Unsupported parameter" error, so the proxy
50887
+ * drops them instead of leaking the incompatibility to the client.
50888
+ */
50889
+ const DEFAULT_REASONING_UNSUPPORTED_PARAMS = ["temperature", "top_p"];
50890
+ /**
50891
+ * A reasoning model is any model that advertises one or more
50892
+ * `reasoning_effort` levels. This dynamically covers the full reasoning
50893
+ * family (mini, codex, future point releases) without a hardcoded ID list.
50894
+ */
50895
+ function isReasoningModel(model) {
50896
+ return modelCache.supportsReasoningEffort(model);
50897
+ }
50898
+ /**
50899
+ * Resolve the set of request parameters to strip for a given model on the
50900
+ * Responses boundary.
50901
+ *
50902
+ * Rule composition:
50903
+ * 1. Built-in default: reasoning models strip {@link DEFAULT_REASONING_UNSUPPORTED_PARAMS}.
50904
+ * Disabled entirely when `responsesApiParameterFiltersReplaceDefault` is true.
50905
+ * 2. User rules (`responsesApiParameterFilters`): every rule whose `models`
50906
+ * glob matches the resolved model id contributes its `params`.
50907
+ *
50908
+ * The result is the union of all matching rules, so user rules ADD to the
50909
+ * default. Setting `responsesApiParameterFiltersReplaceDefault: true` disables
50910
+ * the default so user rules fully OVERWRITE it.
50911
+ */
50912
+ function resolveStrippedResponsesParams(model) {
50913
+ const params = /* @__PURE__ */ new Set();
50914
+ if (!configStore.shouldReplaceDefaultParameterFilters() && isReasoningModel(model)) for (const param of DEFAULT_REASONING_UNSUPPORTED_PARAMS) params.add(param);
50915
+ const modelId = model?.id;
50916
+ if (modelId) {
50917
+ for (const rule of configStore.getResponsesParameterFilters()) if (rule.models.some((pattern) => matchesGlob(pattern, modelId))) for (const param of rule.params) params.add(param);
50918
+ }
50919
+ return params;
50920
+ }
50921
+ /**
50922
+ * Strip unsupported parameters from a Responses payload before dispatch.
50923
+ * Keys are deleted entirely (never set to null) because upstream rejects the
50924
+ * mere presence of the key, not just non-null values.
50925
+ */
50926
+ function applyResponsesParameterFilters(payload, model) {
50927
+ const strip = resolveStrippedResponsesParams(model);
50928
+ if (strip.size === 0) return;
50929
+ const removed = [];
50930
+ for (const key of strip) if (key in payload) {
50931
+ delete payload[key];
50932
+ removed.push(key);
50933
+ }
50934
+ if (removed.length > 0) consola.debug(`Stripped unsupported responses params for model ${model?.id}: ${removed.join(", ")}`);
50935
+ }
50936
+ //#endregion
50868
50937
  //#region src/translator/responses/function-schema.ts
50869
50938
  function isRecord$2(value) {
50870
50939
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -52020,6 +52089,7 @@ const responsesApiEntry = {
52020
52089
  const responsesPayload = withTranslationErrors(() => translateAnthropicToResponsesPayload(ctx.anthropicPayload, { reasoningEffortResolver: (model) => configStore.getReasoningEffort(model) }));
52021
52090
  applyContextManagement(responsesPayload, ctx.selectedModel?.capabilities.limits.max_prompt_tokens);
52022
52091
  compactInputByLatestCompaction(responsesPayload);
52092
+ applyResponsesParameterFilters(responsesPayload, ctx.selectedModel);
52023
52093
  const { vision, initiator } = getResponsesRequestOptions(responsesPayload);
52024
52094
  return await runStrategy(createMessagesViaResponsesStrategy(ctx.copilotClient, responsesPayload, {
52025
52095
  vision,
@@ -52517,6 +52587,7 @@ async function handleResponsesCore({ body, signal, headers }) {
52517
52587
  if (!selectedModel) throwInvalidRequestError("The selected model could not be resolved.", "model");
52518
52588
  if (!modelCache.supportsEndpoint(selectedModel, "/responses")) throwInvalidRequestError("The selected model does not support the responses endpoint.", "model");
52519
52589
  applyContextManagement(payload, selectedModel.capabilities.limits.max_prompt_tokens);
52590
+ applyResponsesParameterFilters(payload, selectedModel);
52520
52591
  },
52521
52592
  buildStrategyContext({ payload, meta, copilotClient, upstreamSignal }) {
52522
52593
  const { vision, initiator } = getResponsesRequestOptions(payload);