Skip to content

Commit 855fac1

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@7661d03cb7077c9e27d725f2e0700148969cb7a9
1 parent e75bf96 commit 855fac1

8 files changed

Lines changed: 400 additions & 21 deletions

File tree

‎common/src/types/contracts/llm.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ export type PromptAiSdkFn = (
165165

166166
export type PromptAiSdkStructuredInput<T> = {
167167
apiKey: string
168+
/** Freebuff's 'free' mode uses platform provider credentials. */
169+
costMode?: string
168170
runId: string
169171
messages: Message[]
170172
schema: z.ZodType<T>

‎sdk/src/__tests__/sponsored-containment-probe.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,35 @@ import { describe, expect, test } from 'bun:test'
22
import { probeSponsoredContainment } from '../tools/sponsored-sandbox'
33

44
describe('sponsored runtime capability probe', () => {
5+
test('probes a Nix profile without /bin/true and mounts its runtime read-only', () => {
6+
const bwrap =
7+
'/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bubblewrap/bin/bwrap'
8+
const paths = new Set([bwrap, '/nix/store', '/bin', '/bin/sh', '/usr'])
9+
const calls: string[][] = []
10+
const result = probeSponsoredContainment('linux', {
11+
exists: (pathname) => paths.has(pathname),
12+
searchPath: '/home/user/.nix-profile/bin',
13+
realpath: (pathname) => {
14+
expect(pathname).toBe('/home/user/.nix-profile/bin/bwrap')
15+
return bwrap
16+
},
17+
execute: (command, args) => {
18+
calls.push([command, ...args])
19+
return true
20+
},
21+
})
22+
expect(result).toEqual({ available: true, mechanism: 'bubblewrap' })
23+
expect(calls).toHaveLength(1)
24+
expect(calls[0]![0]).toBe(bwrap)
25+
const at = calls[0]!.indexOf('/nix/store')
26+
expect(calls[0]!.slice(at - 1, at + 2)).toEqual([
27+
'--ro-bind',
28+
'/nix/store',
29+
'/nix/store',
30+
])
31+
expect(calls[0]!.slice(-4)).toEqual(['--', '/bin/sh', '-c', 'exit 0'])
32+
})
33+
534
test('an installed but unusable Linux namespace boundary refuses before an offer', () => {
635
const calls: string[][] = []
736
const result = probeSponsoredContainment('linux', {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, expect, test } from 'bun:test'
2+
3+
import {
4+
findBubblewrap,
5+
sponsoredLinuxRuntimePath,
6+
sponsoredLinuxSearchPath,
7+
} from '../tools/sponsored-linux-runtime'
8+
9+
const store = '/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
10+
11+
describe('sponsored Linux runtime paths', () => {
12+
test('finds a profile-installed sandbox, ignoring project-local bwrap', () => {
13+
const binary = `${store}-bubblewrap/bin/bwrap`
14+
const links = new Map([
15+
['/project/bin/bwrap', '/project/bin/bwrap'],
16+
['/home/user/.nix-profile/bin/bwrap', binary],
17+
])
18+
expect(
19+
findBubblewrap({
20+
searchPath: '.:relative/bin:/project/bin:/home/user/.nix-profile/bin',
21+
exists: (pathname) => pathname === binary,
22+
realpath: (pathname) => {
23+
const target = links.get(pathname)
24+
if (!target) throw new Error('missing')
25+
return target
26+
},
27+
}),
28+
).toBe(binary)
29+
})
30+
31+
test('preserves the system wrapper rather than bypassing it via PATH', () => {
32+
expect(
33+
findBubblewrap({
34+
searchPath: `${store}-bubblewrap/bin`,
35+
exists: (pathname) => pathname === '/run/wrappers/bin/bwrap',
36+
realpath: () => {
37+
throw new Error('the fixed wrapper must not be resolved')
38+
},
39+
}),
40+
).toBe('/run/wrappers/bin/bwrap')
41+
})
42+
43+
test('never treats an arbitrary PATH executable as the sandbox', () => {
44+
for (const resolved of [
45+
'/project/bin/bwrap',
46+
'/nix/store-copy/bwrap',
47+
'/nix/store/../../project/bwrap',
48+
]) {
49+
expect(
50+
findBubblewrap({
51+
searchPath: '/project/bin',
52+
exists: (pathname) => pathname === resolved,
53+
realpath: () => resolved,
54+
}),
55+
).toBeNull()
56+
}
57+
})
58+
59+
test('uses store paths without granting the profile home or /run', () => {
60+
const profile = `${store}-user-environment/bin`
61+
const bash = `${store}-bash/bin/bash`
62+
const links = new Map([
63+
['/home/user/.nix-profile/bin', profile],
64+
['/run/current-system/sw/bin', `${store}-system-path/bin`],
65+
['/home/user/.nix-profile/bin/bash', bash],
66+
['/usr/bin', '/usr/bin'],
67+
['/project/bin', '/outside/bin'],
68+
])
69+
const realpath = (pathname: string) => {
70+
const target = links.get(pathname)
71+
if (!target) throw new Error('missing')
72+
return target
73+
}
74+
expect(
75+
sponsoredLinuxSearchPath(
76+
'/home/user/.nix-profile/bin:/usr/bin:/run/current-system/sw/bin:/project/bin:/missing:relative:',
77+
realpath,
78+
),
79+
).toBe(
80+
`${profile}:/usr/bin:${store}-system-path/bin:/project/bin:/missing:relative:`,
81+
)
82+
expect(
83+
sponsoredLinuxRuntimePath('/home/user/.nix-profile/bin/bash', realpath),
84+
).toBe(bash)
85+
expect(sponsoredLinuxRuntimePath('bash', realpath)).toBe('bash')
86+
})
87+
})
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import {
2+
BYOK_OPENROUTER_ENV_VAR,
3+
BYOK_OPENROUTER_HEADER,
4+
} from '@codebuff/common/constants/byok'
5+
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test'
6+
import { streamText } from 'ai'
7+
import { z } from 'zod'
8+
9+
import { promptAiSdk, promptAiSdkStream, promptAiSdkStructured } from '../llm'
10+
import { getModelForRequest } from '../model-provider'
11+
12+
const originalKey = process.env[BYOK_OPENROUTER_ENV_VAR]
13+
const legacyKey = 'legacy-openrouter-key-canary'
14+
const content = '{"answer":"ok"}'
15+
const logger = { debug() {}, info() {}, warn() {}, error() {} }
16+
17+
beforeEach(() => {
18+
process.env[BYOK_OPENROUTER_ENV_VAR] = legacyKey
19+
})
20+
21+
afterEach(() => {
22+
if (originalKey === undefined) delete process.env[BYOK_OPENROUTER_ENV_VAR]
23+
else process.env[BYOK_OPENROUTER_ENV_VAR] = originalKey
24+
})
25+
26+
describe('Freebuff provider credentials', () => {
27+
test('an explicitly selected BYOK connection still uses its selected key and endpoint', async () => {
28+
const requests: Array<{ url: string; headers: Headers }> = []
29+
const fetchSpy = spyOn(globalThis, 'fetch').mockImplementation(
30+
(async (input, init) => {
31+
requests.push({
32+
url: String(input),
33+
headers: new Headers(init?.headers),
34+
})
35+
return new Response(
36+
'data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n',
37+
{ headers: { 'content-type': 'text/event-stream' } },
38+
)
39+
}) as typeof fetch,
40+
)
41+
try {
42+
const response = streamText({
43+
model: getModelForRequest({
44+
apiKey: 'freebuff-login-token',
45+
model: 'ignored/model',
46+
costMode: 'free',
47+
byok: {
48+
id: 'connection',
49+
revision: 1,
50+
name: 'My provider',
51+
provider: 'openai-compatible',
52+
baseUrl: 'https://provider.example/v1',
53+
model: 'selected/model',
54+
apiKey: 'explicit-provider-key',
55+
credentialRef: 'connection:connection',
56+
createdAt: 'x',
57+
updatedAt: 'x',
58+
},
59+
}),
60+
messages: [{ role: 'user', content: 'hello' }],
61+
})
62+
expect(await response.text).toBe('ok')
63+
expect(requests).toHaveLength(1)
64+
expect(requests[0]!.url).toBe(
65+
'https://provider.example/v1/chat/completions',
66+
)
67+
expect(requests[0]!.headers.get('authorization')).toBe(
68+
'Bearer explicit-provider-key',
69+
)
70+
expect(requests[0]!.headers.get(BYOK_OPENROUTER_HEADER)).toBeNull()
71+
} finally {
72+
fetchSpy.mockRestore()
73+
}
74+
})
75+
76+
for (const kind of ['stream', 'text', 'structured'] as const) {
77+
test.each(['free', 'normal'])(
78+
`${kind}: provider key selection for %s mode`,
79+
async (costMode) => {
80+
const requests: Array<{ url: string; headers: Headers; body: string }> =
81+
[]
82+
const fetchSpy = spyOn(globalThis, 'fetch').mockImplementation(
83+
(async (input, init) => {
84+
const body = String(init?.body)
85+
requests.push({
86+
url: String(input),
87+
headers: new Headers(init?.headers),
88+
body,
89+
})
90+
if (JSON.parse(body).stream) {
91+
return new Response(
92+
`data: ${JSON.stringify({ choices: [{ delta: { content }, finish_reason: 'stop' }] })}\n\ndata: [DONE]\n\n`,
93+
{ headers: { 'content-type': 'text/event-stream' } },
94+
)
95+
}
96+
return Response.json({
97+
id: 'completion',
98+
model: 'openai/gpt-6.1-sol',
99+
choices: [
100+
{
101+
message: { role: 'assistant', content },
102+
finish_reason: 'stop',
103+
},
104+
],
105+
usage: {
106+
prompt_tokens: 1,
107+
completion_tokens: 1,
108+
total_tokens: 2,
109+
},
110+
})
111+
}) as typeof fetch,
112+
)
113+
try {
114+
const params = {
115+
apiKey: 'freebuff-login-token',
116+
model: 'openai/gpt-6.1-sol',
117+
costMode,
118+
runId: 'run',
119+
clientSessionId: 'session',
120+
fingerprintId: 'fingerprint',
121+
userInputId: 'input',
122+
userId: 'user',
123+
messages: [
124+
{
125+
role: 'user' as const,
126+
content: [{ type: 'text' as const, text: 'hello' }],
127+
},
128+
],
129+
includeCacheControl: false,
130+
sendAction() {},
131+
trackEvent() {},
132+
logger,
133+
signal: new AbortController().signal,
134+
}
135+
if (kind === 'stream') {
136+
for await (const _chunk of promptAiSdkStream(params)) {
137+
// Drain the real SDK stream so its request reaches the fetch boundary.
138+
}
139+
} else if (kind === 'structured') {
140+
await promptAiSdkStructured({
141+
...params,
142+
schema: z.object({ answer: z.string() }),
143+
})
144+
} else {
145+
await promptAiSdk(params)
146+
}
147+
expect(requests).toHaveLength(1)
148+
const request = requests[0]!
149+
expect(new URL(request.url).pathname).toBe('/api/v1/chat/completions')
150+
expect(request.headers.get('authorization')).toBe(
151+
'Bearer freebuff-login-token',
152+
)
153+
expect(JSON.parse(request.body).codebuff_metadata.cost_mode).toBe(
154+
costMode,
155+
)
156+
expect(request.headers.get(BYOK_OPENROUTER_HEADER)).toBe(
157+
costMode === 'free' ? null : legacyKey,
158+
)
159+
expect(request.body).not.toContain(legacyKey)
160+
} finally {
161+
fetchSpy.mockRestore()
162+
}
163+
},
164+
)
165+
}
166+
})

‎sdk/src/impl/llm.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export async function* promptAiSdkStream(
241241
const aiSDKModel = getModelForRequest({
242242
apiKey: params.apiKey,
243243
model: params.model,
244+
costMode: params.costMode,
244245
userId: params.userId,
245246
byok: params.byok,
246247
requestHeaders: params.requestHeaders,
@@ -753,6 +754,7 @@ export async function promptAiSdk(
753754
const aiSDKModel = getModelForRequest({
754755
apiKey: params.apiKey,
755756
model: params.model,
757+
costMode: params.costMode,
756758
userId: params.userId,
757759
byok: params.byok,
758760
requestHeaders: params.requestHeaders,
@@ -835,6 +837,7 @@ export async function promptAiSdkStructured<T>(
835837
const aiSDKModel = getModelForRequest({
836838
apiKey: params.apiKey,
837839
model: params.model,
840+
costMode: params.costMode,
838841
userId: params.userId,
839842
byok: params.byok,
840843
requestHeaders: params.requestHeaders,

‎sdk/src/impl/model-provider.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import path from 'path'
77

88
import { BYOK_OPENROUTER_HEADER } from '@codebuff/common/constants/byok'
9+
import { isFreeMode } from '@codebuff/common/constants/free-agents'
910
import {
1011
FREEBUFF_TURN_SPEND_LIMIT_ERROR_CODE,
1112
FREEBUFF_TURN_SPEND_LIMIT_MESSAGE,
@@ -56,6 +57,8 @@ export interface ModelRequestParams {
5657
apiKey: string
5758
/** Model ID (OpenRouter format, e.g., "anthropic/claude-sonnet-4") */
5859
model: string
60+
/** Freebuff requests use platform credentials, never an inherited provider key. */
61+
costMode?: string
5962
/** End user represented by a trusted service-account request. */
6063
userId?: string
6164
/** Direct, run-scoped credential. Never stored on the model or in metadata. */
@@ -446,6 +449,7 @@ export function withCodebuffRequestHeaders(
446449
export function getModelForRequest({
447450
apiKey,
448451
model,
452+
costMode,
449453
userId,
450454
byok,
451455
requestHeaders,
@@ -557,7 +561,9 @@ export function getModelForRequest({
557561
},
558562
}
559563

560-
const openrouterApiKey = getByokOpenrouterApiKeyFromEnv()
564+
const openrouterApiKey = isFreeMode(costMode)
565+
? undefined
566+
: getByokOpenrouterApiKeyFromEnv()
561567

562568
return new OpenAICompatibleChatLanguageModel(model, {
563569
provider: 'codebuff',

0 commit comments

Comments
 (0)