@velum-labs/routekit-gateway 0.16.8 → 0.17.0

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/server.js CHANGED
@@ -79,6 +79,23 @@ function writeClaudeSelectionError(res, selection) {
79
79
  }
80
80
  });
81
81
  }
82
+ /**
83
+ * Recent Claude Code versions send adaptive thinking and an effort on every
84
+ * turn, including for picker entries that advertise no reasoning controls.
85
+ * The picker is authoritative here: forwarding that implicit client default
86
+ * would make the router reject an otherwise valid model before it reaches its
87
+ * provider. Keep explicit RouteKit effort variants strict, but remove the
88
+ * unrepresentable native default for a base, non-reasoning route.
89
+ */
90
+ function withoutUnsupportedClaudeReasoning(body) {
91
+ const { thinking: _thinking, output_config: outputConfig, ...rest } = body;
92
+ if (outputConfig === undefined || outputConfig === null)
93
+ return rest;
94
+ const { effort: _effort, ...remainingOutputConfig } = outputConfig;
95
+ return Object.keys(remainingOutputConfig).length === 0
96
+ ? rest
97
+ : { ...rest, output_config: remainingOutputConfig };
98
+ }
82
99
  function resolveNativeModelRoute(backend, provider, requested) {
83
100
  if (backend.resolveModelRoute === undefined)
84
101
  return undefined;
@@ -529,13 +546,19 @@ export async function startGateway(options) {
529
546
  // still records attribution and provenance for the rejected request.
530
547
  const route = backend.resolveModelRoute?.(resolvedModel, "claude-code");
531
548
  const canonicalModel = route?.publicId ?? resolvedModel;
549
+ const normalizedRawBody = selection.status === "resolved" &&
550
+ selection.selection.mode === "auto" &&
551
+ route !== undefined &&
552
+ route?.reasoning?.status !== "supported"
553
+ ? withoutUnsupportedClaudeReasoning(rawBody)
554
+ : rawBody;
532
555
  const selectedBody = selection.status === "resolved"
533
- ? withClaudeReasoningSelection(canonicalModel === rawBody.model || canonicalModel === undefined
534
- ? rawBody
535
- : withModel(rawBody, canonicalModel), selection.selection)
536
- : canonicalModel === rawBody.model || canonicalModel === undefined
537
- ? rawBody
538
- : withModel(rawBody, canonicalModel);
556
+ ? withClaudeReasoningSelection(canonicalModel === normalizedRawBody.model || canonicalModel === undefined
557
+ ? normalizedRawBody
558
+ : withModel(normalizedRawBody, canonicalModel), selection.selection)
559
+ : canonicalModel === normalizedRawBody.model || canonicalModel === undefined
560
+ ? normalizedRawBody
561
+ : withModel(normalizedRawBody, canonicalModel);
539
562
  const body = selectedBody;
540
563
  const requestedModel = typeof body.model === "string" ? body.model : undefined;
541
564
  if (anthropicRelay !== undefined &&
@@ -742,6 +742,60 @@ test("estimates tokens and serves Anthropic discovery", async () => {
742
742
  await mock.close();
743
743
  }
744
744
  });
745
+ test("Claude's implicit thinking default does not reject a base model without reasoning controls", async () => {
746
+ const calls = [];
747
+ const backend = await CatalogBackend.create({
748
+ config: {
749
+ providers: { openai: {} },
750
+ defaultModel: "openai/mock-model"
751
+ },
752
+ sources: {
753
+ openai: {
754
+ sourceId: "openai",
755
+ async discoverModels() {
756
+ return [{ id: "mock-model" }];
757
+ },
758
+ async chat(body) {
759
+ calls.push(body);
760
+ return Response.json({
761
+ id: "chatcmpl_no_reasoning",
762
+ choices: [
763
+ {
764
+ index: 0,
765
+ message: { role: "assistant", content: "OK" },
766
+ finish_reason: "stop"
767
+ }
768
+ ],
769
+ usage: { prompt_tokens: 1, completion_tokens: 1 }
770
+ });
771
+ },
772
+ async embeddings() {
773
+ return Response.json({});
774
+ }
775
+ }
776
+ }
777
+ });
778
+ const gateway = await startGateway({ backend });
779
+ try {
780
+ const response = await fetch(`${gateway.url()}/v1/messages`, {
781
+ method: "POST",
782
+ headers: { "content-type": "application/json", "anthropic-version": "2023-06-01" },
783
+ body: JSON.stringify({
784
+ model: "claude-openai/mock-model",
785
+ max_tokens: 32,
786
+ thinking: { type: "adaptive" },
787
+ output_config: { effort: "high" },
788
+ messages: [{ role: "user", content: "hi" }]
789
+ })
790
+ });
791
+ assert.equal(response.status, 200);
792
+ assert.equal(calls.length, 1);
793
+ assert.equal(calls[0]?.reasoning_effort, undefined);
794
+ }
795
+ finally {
796
+ await gateway.close();
797
+ }
798
+ });
745
799
  test("Claude picker aliases use the canonical catalog and pooled native relay", async () => {
746
800
  const sourceCalls = [];
747
801
  const source = (sourceId) => ({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-gateway",
3
3
  "private": false,
4
- "version": "0.16.8",
4
+ "version": "0.17.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/velum-labs/routekit.git",
@@ -29,10 +29,10 @@
29
29
  "@aws-sdk/client-bedrock": "3.1095.0",
30
30
  "@aws-sdk/client-bedrock-runtime": "3.1095.0",
31
31
  "zod": "4.4.3",
32
- "@velum-labs/routekit-contracts": "0.16.8",
33
- "@velum-labs/routekit-registry": "0.16.8",
34
- "@velum-labs/routekit-runtime": "0.16.8",
35
- "@velum-labs/routekit-tracing": "0.16.8"
32
+ "@velum-labs/routekit-contracts": "0.17.0",
33
+ "@velum-labs/routekit-registry": "0.17.0",
34
+ "@velum-labs/routekit-runtime": "0.17.0",
35
+ "@velum-labs/routekit-tracing": "0.17.0"
36
36
  },
37
37
  "keywords": [
38
38
  "routekit",