Skip to content

Commit f86be95

Browse files
committed
Merge remote-tracking branch 'origin/main' into canopy-wave-kimi
# Conflicts: # web/src/llm-api/canopywave.ts
2 parents c117975 + 6dfbb3b commit f86be95

8 files changed

Lines changed: 77 additions & 44 deletions

File tree

cli/src/components/waiting-room-screen.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
9090
forceStart: true,
9191
provider: 'gravity',
9292
fallbackProvider: 'carbon',
93+
surface: 'waiting_room',
9394
})
9495

9596
useFreebuffCtrlCExit()

cli/src/hooks/use-gravity-ad.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export type AdVariant = 'banner' | 'choice'
3535
* same normalized response shape, so the rest of the hook is provider-agnostic.
3636
*/
3737
export type AdProvider = 'gravity' | 'carbon'
38+
export type AdSurface = 'waiting_room'
3839

3940
export type AdData =
4041
| { variant: 'banner'; ad: AdResponse }
@@ -112,11 +113,14 @@ export const useGravityAd = (options?: {
112113
provider?: AdProvider
113114
/** Backup ad network to try when the primary returns no fill or errors. */
114115
fallbackProvider?: AdProvider
116+
/** Product surface requesting the ad. The server maps this to placements. */
117+
surface?: AdSurface
115118
}): GravityAdState => {
116119
const enabled = options?.enabled ?? true
117120
const forceStart = options?.forceStart ?? false
118121
const provider: AdProvider = options?.provider ?? 'gravity'
119122
const fallbackProvider = options?.fallbackProvider
123+
const surface = options?.surface
120124
const [ad, setAd] = useState<AdResponse | null>(null)
121125
const [adData, setAdData] = useState<AdData | null>(null)
122126
const [isLoading, setIsLoading] = useState(false)
@@ -299,6 +303,7 @@ export const useGravityAd = (options?: {
299303
messages: adMessages,
300304
sessionId: useChatStore.getState().chatSessionId,
301305
device: getDeviceInfo(),
306+
...(surface ? { surface } : {}),
302307
// Carbon requires a real browser-ish useragent for targeting/fraud
303308
// detection. Gravity ignores it. We source one centrally so every
304309
// provider that needs it sees the same value.
@@ -430,7 +435,7 @@ export const useGravityAd = (options?: {
430435
clearInterval(id)
431436
ctrlRef.current.intervalId = null
432437
}
433-
}, [shouldStart, shouldHideAds, provider, fallbackProvider])
438+
}, [shouldStart, shouldHideAds, provider, fallbackProvider, surface])
434439

435440
// Don't return ad when ads should be hidden
436441
const visible = shouldStart && !shouldHideAds

freebuff/cli/release/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "freebuff",
3-
"version": "0.0.49",
3+
"version": "0.0.50",
44
"description": "The world's strongest free coding agent",
55
"license": "MIT",
66
"bin": {

scripts/test-canopywave-long.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,10 @@ const MODEL_CONFIGS: Record<string, ModelConfig> = {
3333
outputCostPerToken: 1.20 / 1_000_000,
3434
},
3535
kimi: {
36-
// Pricing is approximate — based on public Moonshot k2 rates; CanopyWave may differ.
3736
id: 'moonshotai/kimi-k2.6',
38-
inputCostPerToken: 0.60 / 1_000_000,
39-
cachedInputCostPerToken: 0.15 / 1_000_000,
40-
outputCostPerToken: 2.50 / 1_000_000,
37+
inputCostPerToken: 0.95 / 1_000_000,
38+
cachedInputCostPerToken: 0.16 / 1_000_000,
39+
outputCostPerToken: 4.00 / 1_000_000,
4140
},
4241
}
4342

web/src/app/api/v1/ads/_post.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@ const deviceSchema = z.object({
3535
})
3636

3737
const providerSchema = z.enum(['gravity', 'carbon']).default('gravity')
38+
const surfaceSchema = z.enum(['waiting_room'])
3839

3940
const bodySchema = z.object({
4041
provider: providerSchema.optional(),
4142
messages: z.array(messageSchema).optional().default([]),
4243
sessionId: z.string().optional(),
4344
device: deviceSchema.optional(),
45+
surface: surfaceSchema.optional(),
4446
/** Browser/CLI useragent passed through to providers that require it. */
4547
userAgent: z.string().optional(),
4648
})
@@ -136,6 +138,7 @@ export async function postAds(params: {
136138
clientIp,
137139
userAgent,
138140
device: parsedBody.device,
141+
surface: parsedBody.surface,
139142
messages: parsedBody.messages,
140143
testMode: serverEnv.CB_ENVIRONMENT !== 'prod',
141144
logger,

web/src/lib/ad-providers/gravity.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ const CHOICE_PLACEMENT_IDS = [
1919
'choice-ad-3',
2020
'choice-ad-4',
2121
]
22+
const WAITING_ROOM_PLACEMENT_IDS = [
23+
'waiting-room-1',
24+
'waiting-room-2',
25+
'waiting-room-3',
26+
'waiting-room-4',
27+
]
2228

2329
type GravityRawAd = {
2430
adText: string
@@ -105,16 +111,21 @@ export function createGravityProvider(config: { apiKey: string }): AdProvider {
105111
fetch,
106112
} = input
107113

108-
const variant = getGravityVariant(userId)
114+
const variant =
115+
input.surface === 'waiting_room' ? 'choice' : getGravityVariant(userId)
109116
const filteredMessages = prepareGravityMessages(messages)
110117

111-
const placements =
112-
variant === 'choice'
113-
? CHOICE_PLACEMENT_IDS.map((id) => ({
114-
placement: 'below_response',
115-
placement_id: id,
116-
}))
117-
: [{ placement: 'below_response', placement_id: BANNER_PLACEMENT_ID }]
118+
const placementIds =
119+
input.surface === 'waiting_room'
120+
? WAITING_ROOM_PLACEMENT_IDS
121+
: variant === 'choice'
122+
? CHOICE_PLACEMENT_IDS
123+
: [BANNER_PLACEMENT_ID]
124+
125+
const placements = placementIds.map((id) => ({
126+
placement: 'below_response',
127+
placement_id: id,
128+
}))
118129

119130
const deviceBody = clientIp
120131
? {

web/src/lib/ad-providers/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export type AdDeviceInfo = {
4141
locale?: string
4242
}
4343

44+
export type AdSurface = 'waiting_room'
45+
4446
export type FetchAdInput = {
4547
userId: string
4648
userEmail: string | null
@@ -50,6 +52,8 @@ export type FetchAdInput = {
5052
/** Browser/CLI useragent string, passed through to upstream. */
5153
userAgent?: string
5254
device?: AdDeviceInfo
55+
/** Product surface requesting the ad. Providers may map this to placements. */
56+
surface?: AdSurface
5357
/** Last user + last preceding assistant message, if any. Used by Gravity. */
5458
messages?: AdMessage[]
5559
/** Set in non-prod so providers can request test ads. */

web/src/llm-api/canopywave.ts

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,52 @@ const canopywaveAgent = new Agent({
2626
bodyTimeout: 0,
2727
})
2828

29-
/** Map from OpenRouter model IDs to CanopyWave model IDs */
30-
const CANOPYWAVE_MODEL_MAP: Record<string, string> = {
31-
'minimax/minimax-m2.5': 'minimax/minimax-m2.5',
32-
'moonshotai/kimi-k2.6': 'moonshotai/kimi-k2.6',
29+
// CanopyWave per-token pricing (dollars per token)
30+
interface CanopyWavePricing {
31+
inputCostPerToken: number
32+
cachedInputCostPerToken: number
33+
outputCostPerToken: number
34+
}
35+
36+
/** Single source of truth: which OpenRouter model IDs we route through
37+
* CanopyWave, the corresponding CanopyWave model ID, and per-model pricing.
38+
* Kept as one map so adding a model can't drift between routing and billing. */
39+
const CANOPYWAVE_MODELS: Record<
40+
string,
41+
{ canopywaveId: string; pricing: CanopyWavePricing }
42+
> = {
43+
'minimax/minimax-m2.5': {
44+
canopywaveId: 'minimax/minimax-m2.5',
45+
pricing: {
46+
inputCostPerToken: 0.27 / 1_000_000,
47+
cachedInputCostPerToken: 0.03 / 1_000_000,
48+
outputCostPerToken: 1.08 / 1_000_000,
49+
},
50+
},
51+
'moonshotai/kimi-k2.6': {
52+
canopywaveId: 'moonshotai/kimi-k2.6',
53+
pricing: {
54+
inputCostPerToken: 0.95 / 1_000_000,
55+
cachedInputCostPerToken: 0.16 / 1_000_000,
56+
outputCostPerToken: 4.00 / 1_000_000,
57+
},
58+
},
3359
}
3460

3561
export function isCanopyWaveModel(model: string): boolean {
36-
return model in CANOPYWAVE_MODEL_MAP
62+
return model in CANOPYWAVE_MODELS
3763
}
3864

3965
function getCanopyWaveModelId(openrouterModel: string): string {
40-
return CANOPYWAVE_MODEL_MAP[openrouterModel] ?? openrouterModel
66+
return CANOPYWAVE_MODELS[openrouterModel]?.canopywaveId ?? openrouterModel
67+
}
68+
69+
function getCanopyWavePricing(model: string): CanopyWavePricing {
70+
const entry = CANOPYWAVE_MODELS[model]
71+
if (!entry) {
72+
throw new Error(`No CanopyWave pricing found for model: ${model}`)
73+
}
74+
return entry.pricing
4175
}
4276

4377
type StreamState = { responseText: string; reasoningText: string; ttftMs: number | null; billedAlready: boolean }
@@ -86,30 +120,6 @@ function createCanopyWaveRequest(params: {
86120
})
87121
}
88122

89-
// CanopyWave per-token pricing (dollars per token), keyed by OpenRouter model ID
90-
interface CanopyWavePricing {
91-
inputCostPerToken: number
92-
cachedInputCostPerToken: number
93-
outputCostPerToken: number
94-
}
95-
96-
const CANOPYWAVE_PRICING_MAP: Record<string, CanopyWavePricing> = {
97-
'minimax/minimax-m2.5': {
98-
inputCostPerToken: 0.27 / 1_000_000,
99-
cachedInputCostPerToken: 0.03 / 1_000_000,
100-
outputCostPerToken: 1.08 / 1_000_000,
101-
},
102-
'moonshotai/kimi-k2.6': {
103-
inputCostPerToken: 0.60 / 1_000_000,
104-
cachedInputCostPerToken: 0.15 / 1_000_000,
105-
outputCostPerToken: 2.50 / 1_000_000,
106-
},
107-
}
108-
109-
function getCanopyWavePricing(model: string): CanopyWavePricing {
110-
return CANOPYWAVE_PRICING_MAP[model] ?? CANOPYWAVE_PRICING_MAP['moonshotai/kimi-k2.6']
111-
}
112-
113123
function extractUsageAndCost(usage: Record<string, unknown> | undefined | null, model: string): UsageData {
114124
if (!usage) return { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, reasoningTokens: 0, cost: 0 }
115125
const promptDetails = usage.prompt_tokens_details as Record<string, unknown> | undefined | null

0 commit comments

Comments
 (0)