Skip to content

fix: patch openrouter snake_case options to map to camelCase#316

Open
tombeckenham wants to merge 3 commits intoTanStack:mainfrom
tombeckenham:openrouter-patch-fix
Open

fix: patch openrouter snake_case options to map to camelCase#316
tombeckenham wants to merge 3 commits intoTanStack:mainfrom
tombeckenham:openrouter-patch-fix

Conversation

@tombeckenham
Copy link
Contributor

@tombeckenham tombeckenham commented Feb 25, 2026

Fixes #314

🎯 Changes

Transforms all snake_case model options to camelCase in the open router adapter to ensure optins are passed through the SDK to openrouter

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • New Features

    • Model options now automatically convert to camelCase format for API compatibility
    • Enhanced handling of undefined options prevents incomplete spreads
  • Tests

    • Added comprehensive serialization tests to validate model options format transformation and wire-format compatibility

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

Introduces a utility function to convert snake_case keys to camelCase in model options and applies this transformation in the OpenRouter text adapter to ensure proper serialization before passing to the SDK. Updates tests to validate the conversion and wire-format serialization.

Changes

Cohort / File(s) Summary
Changeset Documentation
.changeset/bright-geese-poke.md
Documents a patch release for snake_case to camelCase mapping adjustment in model options.
Casing Utility
packages/typescript/ai-openrouter/src/utils/casing.ts
New utility module with snakeToCamel() helper and exported snakeToCamelOptions() function to recursively transform object keys from snake_case to camelCase.
Utils Export
packages/typescript/ai-openrouter/src/utils/index.ts
Re-exports the new snakeToCamelOptions function for public access.
Text Adapter Integration
packages/typescript/ai-openrouter/src/adapters/text.ts
Updates the OpenRouter text adapter to apply snakeToCamelOptions() transformation to model options before passing to SDK, replacing direct spread of options.
Test Suite
packages/typescript/ai-openrouter/tests/openrouter-adapter.test.ts
Updates existing test to validate camelCase handling and adds new test suite to verify snake_case model options survive SDK serialization and correctly map to wire-format keys.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested reviewers

  • harry-whorlow

Poem

🐰 A rabbit hops with glee today,
Snake_case turning camelCase way!
Options transform with elegant grace,
No silent fields left behind in place,
The adapter now speaks the SDK's true tongue! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing the OpenRouter adapter to map snake_case options to camelCase for proper SDK handling.
Description check ✅ Passed The PR description follows the template with all required sections completed, references issue #314, explains the fix clearly, and confirms checklist items including changeset generation.
Linked Issues check ✅ Passed The PR implementation directly addresses issue #314 by implementing option 2 (transforming modelOptions from snake_case to camelCase in the adapter), with utilities added and tests updated to verify the transformation.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue #314: new casing utility, adapter modifications to use the utility, tests updated to verify the fix, and a changeset entry documenting the patch.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link

nx-cloud bot commented Feb 25, 2026

View your CI Pipeline Execution ↗ for commit 4a09d2f

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 59s View ↗
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 41s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-25 02:59:04 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 25, 2026

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@316

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@316

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@316

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@316

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@316

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@316

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@316

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@316

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@316

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@316

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@316

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@316

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@316

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@316

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@316

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@316

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@316

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@316

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@316

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@316

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@316

commit: 4a09d2f

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/typescript/ai-openrouter/src/utils/casing.ts (1)

5-11: Preserve typing in snakeToCamelOptions return value.

Returning Record<string, unknown> weakens compile-time safety for provider option mapping. A generic mapped return type keeps this utility type-safe where it is spread into SDK params.

🧩 Suggested typed version
+type SnakeToCamelCase<S extends string> =
+  S extends `${infer Head}_${infer Tail}`
+    ? `${Head}${Capitalize<SnakeToCamelCase<Tail>>}`
+    : S
+
-export const snakeToCamelOptions = (obj: Record<string, unknown>) => {
+export const snakeToCamelOptions = <T extends Record<string, unknown>>(
+  obj: T,
+) => {
   return Object.fromEntries(
     Object.entries(obj).map(([key, value]) => {
       return [snakeToCamel(key), value]
     }),
-  )
+  ) as {
+    [K in keyof T as K extends string ? SnakeToCamelCase<K> : K]: T[K]
+  }
 }

As per coding guidelines, "Use type-safe per-model configuration with provider options typed based on selected model to ensure compile-time safety".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-openrouter/src/utils/casing.ts` around lines 5 - 11,
snakeToCamelOptions currently loses type information by returning a generic
Record<string, unknown>; change it to a generic function so the input type is
preserved and the output keys are the camel-cased version of the input keys
while values keep their original types. Update snakeToCamelOptions to accept a
generic type parameter (e.g., T extends Record<string, unknown>) and return a
mapped type that remaps each key K in keyof T to the camel-cased key (using the
existing snakeToCamel function for the transformation conceptually) with the
same value type T[K], so callers retain compile-time safety when spreading into
SDK params. Ensure typings and exports are updated accordingly without changing
runtime behavior.
packages/typescript/ai-openrouter/src/adapters/text.ts (1)

525-534: Exclude variant from spread model options.

variant is already baked into model on Line 527-528. Spreading all mapped modelOptions on Line 533 forwards variant again as a separate param, which is redundant and can become ambiguous later.

♻️ Suggested refinement
-    const modelOptions = options.modelOptions as
+    const modelOptions = options.modelOptions as
       | Omit<InternalTextProviderOptions, 'model' | 'messages' | 'tools'>
       | undefined
+    const { variant, ...providerOptions } = modelOptions ?? {}
@@
-      model:
-        options.model +
-        (modelOptions?.variant ? `:${modelOptions.variant}` : ''),
+      model: options.model + (variant ? `:${variant}` : ''),
@@
-      ...snakeToCamelOptions(modelOptions ?? {}),
+      ...snakeToCamelOptions(providerOptions),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-openrouter/src/adapters/text.ts` around lines 525 -
534, The ChatGenerationParams construction currently appends
modelOptions?.variant into the model string and then spreads
snakeToCamelOptions(modelOptions) (in the request object building code), which
re-introduces variant; update the construction so the spread excludes the
variant key—e.g., filter out variant from modelOptions before calling
snakeToCamelOptions or remove variant from the resulting object—so that only the
combined model string (options.model + ':' + variant) is sent and no separate
variant property is forwarded.
packages/typescript/ai-openrouter/tests/openrouter-adapter.test.ts (1)

874-889: Assert adapter-side camelCase before outbound schema parse.

This test currently verifies wire-format snake_case after schema parsing. Add assertions on params first (frequencyPenalty, presencePenalty, toolChoice, maxCompletionTokens) so adapter mapping is validated directly, independent of serializer behavior.

✅ Suggested assertion additions
     const [rawParams] = mockSend.mock.calls[0]!
     const params = rawParams
+
+    expect(params).toHaveProperty('frequencyPenalty', 0.5)
+    expect(params).toHaveProperty('presencePenalty', 0.3)
+    expect(params).toHaveProperty('toolChoice', 'auto')
+    expect(params).toHaveProperty('maxCompletionTokens', 4096)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-openrouter/tests/openrouter-adapter.test.ts` around
lines 874 - 889, The test currently asserts snake_case keys on the serialized
result but should first assert the adapter produced camelCase keys; before
calling ChatGenerationParams$outboundSchema.parse(rawParams) add assertions that
params (or rawParams) has frequencyPenalty: 0.5, presencePenalty: 0.3,
toolChoice: 'auto', and maxCompletionTokens: 4096 so the adapter mapping is
validated independently of the outbound schema.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/typescript/ai-openrouter/src/adapters/text.ts`:
- Around line 525-534: The ChatGenerationParams construction currently appends
modelOptions?.variant into the model string and then spreads
snakeToCamelOptions(modelOptions) (in the request object building code), which
re-introduces variant; update the construction so the spread excludes the
variant key—e.g., filter out variant from modelOptions before calling
snakeToCamelOptions or remove variant from the resulting object—so that only the
combined model string (options.model + ':' + variant) is sent and no separate
variant property is forwarded.

In `@packages/typescript/ai-openrouter/src/utils/casing.ts`:
- Around line 5-11: snakeToCamelOptions currently loses type information by
returning a generic Record<string, unknown>; change it to a generic function so
the input type is preserved and the output keys are the camel-cased version of
the input keys while values keep their original types. Update
snakeToCamelOptions to accept a generic type parameter (e.g., T extends
Record<string, unknown>) and return a mapped type that remaps each key K in
keyof T to the camel-cased key (using the existing snakeToCamel function for the
transformation conceptually) with the same value type T[K], so callers retain
compile-time safety when spreading into SDK params. Ensure typings and exports
are updated accordingly without changing runtime behavior.

In `@packages/typescript/ai-openrouter/tests/openrouter-adapter.test.ts`:
- Around line 874-889: The test currently asserts snake_case keys on the
serialized result but should first assert the adapter produced camelCase keys;
before calling ChatGenerationParams$outboundSchema.parse(rawParams) add
assertions that params (or rawParams) has frequencyPenalty: 0.5,
presencePenalty: 0.3, toolChoice: 'auto', and maxCompletionTokens: 4096 so the
adapter mapping is validated independently of the outbound schema.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f800aa and 4a09d2f.

📒 Files selected for processing (5)
  • .changeset/bright-geese-poke.md
  • packages/typescript/ai-openrouter/src/adapters/text.ts
  • packages/typescript/ai-openrouter/src/utils/casing.ts
  • packages/typescript/ai-openrouter/src/utils/index.ts
  • packages/typescript/ai-openrouter/tests/openrouter-adapter.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenRouter model options should be camelCase

1 participant