Skip to content

Add OpenRouter provider support#51

Open
qwadratic wants to merge 1 commit intofarzaa:mainfrom
qwadratic:feature/openrouter-support
Open

Add OpenRouter provider support#51
qwadratic wants to merge 1 commit intofarzaa:mainfrom
qwadratic:feature/openrouter-support

Conversation

@qwadratic
Copy link
Copy Markdown

Summary

Adds an optional OpenRouter dropdown to the companion panel so users can route voice requests through OpenRouter (Sonnet, Opus, ChatGPT, Grok, Gemini, Qwen) instead of Claude direct. Default behavior is unchanged — OpenRouter is opt-in.

Changes

Cloudflare Worker (worker/src/index.ts)

  • New /openrouter-chat route that proxies to https://openrouter.ai/api/v1/chat/completions
  • New OPENROUTER_API_KEY in the Env interface

Swift app

  • New OpenRouterAPI.swift — OpenAI-compatible streaming client (parses choices[0].delta.content SSE events, uses image_url data URIs for vision)
  • CompanionManager: new selectedOpenRouterModel (persisted to UserDefaults), isUsingOpenRouter computed property, and analyzeImageStreamingWithActiveProvider() helper that routes to the right client
  • CompanionPanelView: new "OpenRouter" row with a dropdown menu (Off / Sonnet / Opus / ChatGPT / Grok / Gemini / Qwen). Selecting an OpenRouter model deselects the Claude direct buttons; selecting a Claude direct button clears OpenRouter.

Setup (required before this works)

Merging this PR requires a worker redeploy and a new secret:

cd worker
npx wrangler secret put OPENROUTER_API_KEY
npx wrangler deploy

Without the secret, selecting an OpenRouter model will error — but the default (Claude direct) keeps working, so the blast radius of "merged but not deployed" is low.

Design notes

  • OpenRouter is fully opt-in. Fresh installs still default to Claude Sonnet direct, same as before.
  • The OpenRouter selection is persisted in UserDefaults under selectedOpenRouterModel (separate key from selectedClaudeModel) so the two pickers don't clobber each other.
  • The model list (Sonnet, Opus, ChatGPT, Grok, Gemini, Qwen) is hardcoded in CompanionPanelView.openRouterModels. Easy to edit if you want different defaults.

Test plan

  • Build in Xcode, app launches without errors
  • Open the panel — both "Model" (Sonnet/Opus) and "OpenRouter" (dropdown) rows appear
  • Select an OpenRouter model, push-to-talk, verify response streams and TTS plays
  • Switch to a different OpenRouter model (e.g. Gemini), verify a distinctly different voice/style
  • Switch OpenRouter dropdown back to "Off", verify Claude direct still works
  • Xcode console shows 🌐 OpenRouter streaming request: when OpenRouter is active, and 🌐 Claude streaming request: when it's off

Adds an optional OpenRouter dropdown to the companion panel so users can
route voice requests through OpenRouter (Sonnet, Opus, ChatGPT, Grok,
Gemini, Qwen) instead of Claude direct. Default behavior is unchanged —
OpenRouter is opt-in.

- New /openrouter-chat route on the Cloudflare Worker proxy
- New OpenRouterAPI.swift client using OpenAI-compatible streaming format
- Provider-aware routing in CompanionManager
- OpenRouter model picker dropdown in CompanionPanelView

Requires OPENROUTER_API_KEY worker secret to be set and worker redeploy.
@Qodo-Free-For-OSS
Copy link
Copy Markdown

Hi, The OpenRouter model dropdown uses ForEach(..., id: \.modelID) but the collection elements are tuples, and Swift key paths can’t reference tuple members, causing a compile-time build failure.

Severity: action required | Category: correctness

How to fix: Use struct or explicit id

Agent prompt to fix - you can give this to your LLM of choice:

Issue description

SwiftUI ForEach uses a key path \.modelID on tuple elements, which does not compile.

Issue Context

openRouterModels is [(label: String, modelID: String)] (tuple array).

Fix Focus Areas

  • leanring-buddy/CompanionPanelView.swift[649-692]

What to change

Choose one:

  1. Replace the tuple with a small struct:

    • struct OpenRouterModel: Identifiable { let id: String; let label: String }
    • Update array to [OpenRouterModel] and ForEach(Self.openRouterModels) { model in ... }
  2. Keep tuples but avoid KeyPath:

    • Use ForEach(Self.openRouterModels, id: \. self) and ensure uniqueness, then refer to tuple elements directly.

(Option 1 is clearer/safer.)


Qodo code review - free for open-source.

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.

2 participants