Skip to content

Route Kimi K2.6 requests through CanopyWave#550

Merged
jahooma merged 3 commits intomainfrom
canopywave-routing
Apr 25, 2026
Merged

Route Kimi K2.6 requests through CanopyWave#550
jahooma merged 3 commits intomainfrom
canopywave-routing

Conversation

@jahooma
Copy link
Copy Markdown
Contributor

@jahooma jahooma commented Apr 25, 2026

Summary

  • Backend-only wiring for routing `moonshotai/kimi-k2.6` through CanopyWave instead of OpenRouter.
  • No behavior change in production — nothing in the codebase currently requests `moonshotai/kimi-k2.6`. This PR just makes the model available so a follow-up can flip the freebuff "smart" model.
  • Adds Kimi K2.6 to `CANOPYWAVE_MODEL_MAP` and gives CanopyWave a per-model pricing map (Kimi: ~$0.60/$0.15/$2.50 per 1M in/cache/out).
  • Flips `useCanopyWave` from `false` to `isCanopyWaveModel(...)` in the chat-completions endpoint (stream + non-stream).

Why this is a no-op today

  • `isCanopyWaveModel` only returns true for `minimax/minimax-m2.5` and `moonshotai/kimi-k2.6`. Neither model is referenced by any agent or by the freebuff model selector right now.
  • Every other model (Fireworks, OpenAI direct, OpenRouter) hits the same routing branch as before.

Test plan

  • `bun tsc --noEmit` passes for web
  • `bun test src/llm-api src/app/api/v1/chat/completions` — all 83 existing tests pass
  • After merge: `bun scripts/test-canopywave.ts both` to smoke-test Kimi → CanopyWave end-to-end against the deployed web instance

Follow-up

🤖 Generated with Claude Code

Backend-only wiring. No agent or freebuff-model changes — current
behavior is unchanged because nothing in the codebase requests
moonshotai/kimi-k2.6 yet. Sets the stage for switching the freebuff
"smart" model in a follow-up PR.

- Add moonshotai/kimi-k2.6 to CANOPYWAVE_MODEL_MAP so isCanopyWaveModel
  picks it up.
- Refactor canopywave pricing into a per-model map and add Kimi pricing
  ($0.60/$0.15/$2.50 per 1M in/cache/out, approximate Moonshot rates).
- Flip useCanopyWave from `false` to isCanopyWaveModel(...) in
  _post.ts (stream + non-stream). For models not in the map this is a
  no-op — only minimax-m2.5 and kimi-k2.6 are affected, neither of
  which is currently used.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 25, 2026

Greptile Summary

This PR wires moonshotai/kimi-k2.6 through CanopyWave by adding it to the model map, introducing a per-model pricing structure, and removing the false override that kept CanopyWave routing disabled. The routing change in _post.ts is clean and correct.

  • P1 billing risk: getCanopyWavePricing silently falls back to Kimi K2.6 pricing for any unknown model. Since CANOPYWAVE_MODEL_MAP and CANOPYWAVE_PRICING_MAP are kept in sync manually, a future drift would cause wrong costs to be computed without any error. The fallback should throw instead.
  • P2 simplification: The two parallel maps could be merged into a single CANOPYWAVE_MODELS record (model ID + pricing per entry), eliminating the drift risk entirely and reducing boilerplate.

Confidence Score: 4/5

Safe to merge once the silent billing fallback in getCanopyWavePricing is addressed.

One P1 finding: the fallback to Kimi pricing for unrecognized models will silently compute wrong billing if the two maps ever diverge. The routing changes themselves are correct and the PR is currently a no-op in production, but fixing the fallback before merge prevents a harder-to-catch billing bug in follow-up PRs.

web/src/llm-api/canopywave.ts — the getCanopyWavePricing fallback logic.

Important Files Changed

Filename Overview
web/src/llm-api/canopywave.ts Adds Kimi K2.6 to the CanopyWave model map and introduces a per-model pricing map; the fallback to Kimi pricing in getCanopyWavePricing is a silent billing risk if the two parallel maps drift out of sync.
web/src/app/api/v1/chat/completions/_post.ts Enables live CanopyWave routing by replacing the hardcoded false with isCanopyWaveModel(...) and correctly short-circuits Fireworks/OpenAI-Direct checks for both streaming and non-streaming paths.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: web/src/llm-api/canopywave.ts
Line: 109-111

Comment:
**Silent fallback to wrong pricing on map drift**

`getCanopyWavePricing` falls back to Kimi K2.6 pricing for any model that isn't in `CANOPYWAVE_PRICING_MAP`. Because `CANOPYWAVE_MODEL_MAP` and `CANOPYWAVE_PRICING_MAP` must be kept manually in sync, a future model added to only one of the two maps would silently bill at Kimi rates — either under-charging (MiniMax rates for Kimi) or over-charging (Kimi rates for a cheaper model). Since the model reaching this function is always a confirmed CanopyWave model, the fallback can be removed and replaced with an explicit error:

```ts
function getCanopyWavePricing(model: string): CanopyWavePricing {
  const pricing = CANOPYWAVE_PRICING_MAP[model]
  if (!pricing) throw new Error(`No CanopyWave pricing found for model: ${model}`)
  return pricing
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: web/src/llm-api/canopywave.ts
Line: 29-107

Comment:
**Simplify: merge the two parallel maps into one**

`CANOPYWAVE_MODEL_MAP` and `CANOPYWAVE_PRICING_MAP` always need to be updated together, and the model IDs happen to be identical across both maps. Keeping them separate creates a drift risk (flagged above). Merging them into a single structure eliminates the redundancy and makes `isCanopyWaveModel`, `getCanopyWaveModelId`, and `getCanopyWavePricing` all hit the same source of truth:

```ts
const CANOPYWAVE_MODELS: Record<string, { canopywaveId: string; pricing: CanopyWavePricing }> = {
  'minimax/minimax-m2.5': {
    canopywaveId: 'minimax/minimax-m2.5',
    pricing: { inputCostPerToken: 0.27 / 1_000_000, cachedInputCostPerToken: 0.03 / 1_000_000, outputCostPerToken: 1.08 / 1_000_000 },
  },
  'moonshotai/kimi-k2.6': {
    canopywaveId: 'moonshotai/kimi-k2.6',
    pricing: { inputCostPerToken: 0.60 / 1_000_000, cachedInputCostPerToken: 0.15 / 1_000_000, outputCostPerToken: 2.50 / 1_000_000 },
  },
}

export function isCanopyWaveModel(model: string): boolean { return model in CANOPYWAVE_MODELS }
function getCanopyWaveModelId(m: string): string { return CANOPYWAVE_MODELS[m]?.canopywaveId ?? m }
function getCanopyWavePricing(model: string): CanopyWavePricing {
  const entry = CANOPYWAVE_MODELS[model]
  if (!entry) throw new Error(`No CanopyWave pricing found for model: ${model}`)
  return entry.pricing
}
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Route Kimi K2.6 requests through CanopyW..." | Re-trigger Greptile

Comment thread web/src/llm-api/canopywave.ts Outdated
Comment on lines +109 to +111
function getCanopyWavePricing(model: string): CanopyWavePricing {
return CANOPYWAVE_PRICING_MAP[model] ?? CANOPYWAVE_PRICING_MAP['moonshotai/kimi-k2.6']
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Silent fallback to wrong pricing on map drift

getCanopyWavePricing falls back to Kimi K2.6 pricing for any model that isn't in CANOPYWAVE_PRICING_MAP. Because CANOPYWAVE_MODEL_MAP and CANOPYWAVE_PRICING_MAP must be kept manually in sync, a future model added to only one of the two maps would silently bill at Kimi rates — either under-charging (MiniMax rates for Kimi) or over-charging (Kimi rates for a cheaper model). Since the model reaching this function is always a confirmed CanopyWave model, the fallback can be removed and replaced with an explicit error:

function getCanopyWavePricing(model: string): CanopyWavePricing {
  const pricing = CANOPYWAVE_PRICING_MAP[model]
  if (!pricing) throw new Error(`No CanopyWave pricing found for model: ${model}`)
  return pricing
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/llm-api/canopywave.ts
Line: 109-111

Comment:
**Silent fallback to wrong pricing on map drift**

`getCanopyWavePricing` falls back to Kimi K2.6 pricing for any model that isn't in `CANOPYWAVE_PRICING_MAP`. Because `CANOPYWAVE_MODEL_MAP` and `CANOPYWAVE_PRICING_MAP` must be kept manually in sync, a future model added to only one of the two maps would silently bill at Kimi rates — either under-charging (MiniMax rates for Kimi) or over-charging (Kimi rates for a cheaper model). Since the model reaching this function is always a confirmed CanopyWave model, the fallback can be removed and replaced with an explicit error:

```ts
function getCanopyWavePricing(model: string): CanopyWavePricing {
  const pricing = CANOPYWAVE_PRICING_MAP[model]
  if (!pricing) throw new Error(`No CanopyWave pricing found for model: ${model}`)
  return pricing
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread web/src/llm-api/canopywave.ts Outdated
Comment on lines +29 to +107
@@ -85,12 +86,31 @@ function createCanopyWaveRequest(params: {
})
}

// CanopyWave per-token pricing (dollars per token) for MiniMax M2.5
const CANOPYWAVE_INPUT_COST_PER_TOKEN = 0.27 / 1_000_000
const CANOPYWAVE_CACHED_INPUT_COST_PER_TOKEN = 0.03 / 1_000_000
const CANOPYWAVE_OUTPUT_COST_PER_TOKEN = 1.08 / 1_000_000
// CanopyWave per-token pricing (dollars per token), keyed by OpenRouter model ID
interface CanopyWavePricing {
inputCostPerToken: number
cachedInputCostPerToken: number
outputCostPerToken: number
}

const CANOPYWAVE_PRICING_MAP: Record<string, CanopyWavePricing> = {
'minimax/minimax-m2.5': {
inputCostPerToken: 0.27 / 1_000_000,
cachedInputCostPerToken: 0.03 / 1_000_000,
outputCostPerToken: 1.08 / 1_000_000,
},
'moonshotai/kimi-k2.6': {
inputCostPerToken: 0.60 / 1_000_000,
cachedInputCostPerToken: 0.15 / 1_000_000,
outputCostPerToken: 2.50 / 1_000_000,
},
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Simplify: merge the two parallel maps into one

CANOPYWAVE_MODEL_MAP and CANOPYWAVE_PRICING_MAP always need to be updated together, and the model IDs happen to be identical across both maps. Keeping them separate creates a drift risk (flagged above). Merging them into a single structure eliminates the redundancy and makes isCanopyWaveModel, getCanopyWaveModelId, and getCanopyWavePricing all hit the same source of truth:

const CANOPYWAVE_MODELS: Record<string, { canopywaveId: string; pricing: CanopyWavePricing }> = {
  'minimax/minimax-m2.5': {
    canopywaveId: 'minimax/minimax-m2.5',
    pricing: { inputCostPerToken: 0.27 / 1_000_000, cachedInputCostPerToken: 0.03 / 1_000_000, outputCostPerToken: 1.08 / 1_000_000 },
  },
  'moonshotai/kimi-k2.6': {
    canopywaveId: 'moonshotai/kimi-k2.6',
    pricing: { inputCostPerToken: 0.60 / 1_000_000, cachedInputCostPerToken: 0.15 / 1_000_000, outputCostPerToken: 2.50 / 1_000_000 },
  },
}

export function isCanopyWaveModel(model: string): boolean { return model in CANOPYWAVE_MODELS }
function getCanopyWaveModelId(m: string): string { return CANOPYWAVE_MODELS[m]?.canopywaveId ?? m }
function getCanopyWavePricing(model: string): CanopyWavePricing {
  const entry = CANOPYWAVE_MODELS[model]
  if (!entry) throw new Error(`No CanopyWave pricing found for model: ${model}`)
  return entry.pricing
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/llm-api/canopywave.ts
Line: 29-107

Comment:
**Simplify: merge the two parallel maps into one**

`CANOPYWAVE_MODEL_MAP` and `CANOPYWAVE_PRICING_MAP` always need to be updated together, and the model IDs happen to be identical across both maps. Keeping them separate creates a drift risk (flagged above). Merging them into a single structure eliminates the redundancy and makes `isCanopyWaveModel`, `getCanopyWaveModelId`, and `getCanopyWavePricing` all hit the same source of truth:

```ts
const CANOPYWAVE_MODELS: Record<string, { canopywaveId: string; pricing: CanopyWavePricing }> = {
  'minimax/minimax-m2.5': {
    canopywaveId: 'minimax/minimax-m2.5',
    pricing: { inputCostPerToken: 0.27 / 1_000_000, cachedInputCostPerToken: 0.03 / 1_000_000, outputCostPerToken: 1.08 / 1_000_000 },
  },
  'moonshotai/kimi-k2.6': {
    canopywaveId: 'moonshotai/kimi-k2.6',
    pricing: { inputCostPerToken: 0.60 / 1_000_000, cachedInputCostPerToken: 0.15 / 1_000_000, outputCostPerToken: 2.50 / 1_000_000 },
  },
}

export function isCanopyWaveModel(model: string): boolean { return model in CANOPYWAVE_MODELS }
function getCanopyWaveModelId(m: string): string { return CANOPYWAVE_MODELS[m]?.canopywaveId ?? m }
function getCanopyWavePricing(model: string): CanopyWavePricing {
  const entry = CANOPYWAVE_MODELS[model]
  if (!entry) throw new Error(`No CanopyWave pricing found for model: ${model}`)
  return entry.pricing
}
```

How can I resolve this? If you propose a fix, please make it concise.

jahooma and others added 2 commits April 25, 2026 14:36
Combine CANOPYWAVE_MODEL_MAP and CANOPYWAVE_PRICING_MAP into a single
CANOPYWAVE_MODELS map keyed by OpenRouter model ID. Removes the silent
Kimi-pricing fallback in getCanopyWavePricing — it now throws on
unknown models, since callers are expected to gate on isCanopyWaveModel
first. Eliminates the drift risk if a future model is added to one
map but not the other.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Confirmed CanopyWave rates. Was using approximate Moonshot K2 numbers
as a placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jahooma jahooma merged commit 6dfbb3b into main Apr 25, 2026
19 checks passed
@jahooma jahooma deleted the canopywave-routing branch April 25, 2026 21:46
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.

1 participant