Skip to content

Commit d2a3736

Browse files
committed
Support Venice Kimi model completions
1 parent c5650d4 commit d2a3736

3 files changed

Lines changed: 53 additions & 5 deletions

File tree

LLM.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ All live in the repo-root **`.env`** (gitignored - never committed). `.env.examp
5252
Default models (override with `LLM_MODEL`): Venice `llama-3.3-70b` — OpenAI `gpt-4o-mini` — Anthropic
5353
`claude-haiku-4-5-20251001`.
5454

55+
Venice code/reasoning models such as `kimi-k2-7-code` work too:
56+
57+
```ini
58+
LLM_PROVIDER=venice
59+
VENICE_API_KEY=...
60+
LLM_MODEL=kimi-k2-7-code
61+
```
62+
63+
Kimi may spend a small completion budget on internal reasoning before emitting `message.content`, so
64+
the runtime automatically raises too-low Venice/Kimi `maxTokens` requests to `1024`. Other providers
65+
and non-Kimi Venice models keep the caller's requested budget.
66+
5567
## Set it up — copy/paste
5668

5769
**Venice AI — the kit's LLM (recommended, free credits):**

packages/agent-runtime/src/llm/complete.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, afterEach, vi } from 'vitest'
2-
import { pickProvider, parseJsonReply, complete } from './complete.js'
2+
import { pickProvider, parseJsonReply, complete, effectiveMaxTokens } from './complete.js'
33

44
const env = { ...process.env }
55
afterEach(() => {
@@ -60,6 +60,29 @@ describe('complete model resolution', () => {
6060
}
6161
expect(sentModel).toBe('claude-haiku-4-5-20251001') // not "" (which Anthropic 400s)
6262
})
63+
64+
it('raises too-small Venice Kimi budgets so reasoning models can emit content', async () => {
65+
process.env.LLM_PROVIDER = 'venice'
66+
process.env.VENICE_API_KEY = 'k'
67+
process.env.LLM_MODEL = 'kimi-k2-7-code'
68+
let sentMaxTokens = 0
69+
const realFetch = global.fetch
70+
global.fetch = vi.fn(async (_url: unknown, init?: { body?: string }) => {
71+
sentMaxTokens = JSON.parse(init?.body ?? '{}').max_tokens
72+
return { ok: true, json: async () => ({ choices: [{ message: { content: '{"ok":true}' } }] }) }
73+
}) as unknown as typeof fetch
74+
try {
75+
await complete({ system: 's', user: 'u', maxTokens: 120 })
76+
} finally {
77+
global.fetch = realFetch
78+
}
79+
expect(sentMaxTokens).toBe(1024)
80+
})
81+
82+
it('leaves non-Kimi token budgets alone', () => {
83+
expect(effectiveMaxTokens('venice', 'llama-3.3-70b', 120)).toBe(120)
84+
expect(effectiveMaxTokens('openai', 'kimi-k2-7-code', 120)).toBe(120)
85+
})
6386
})
6487

6588
describe('parseJsonReply', () => {

packages/agent-runtime/src/llm/complete.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
* LLM pillar — one provider-agnostic `complete()` call.
33
*
44
* SDK-free (`fetch`-based) so the runtime stays dependency-light. Provider is chosen by env, so the
5-
* whole market flips from Anthropic (dev default) to the sponsored OpenAI key with `LLM_PROVIDER=openai`
6-
* (or to Venice AI with `LLM_PROVIDER=venice`) and no code change. Callers ask for a single JSON-shaped
5+
* whole market flips to Venice AI with `LLM_PROVIDER=venice` (or OpenAI/Anthropic) and no code change.
6+
* Callers ask for a single JSON-shaped
77
* answer and enforce their own guards on it — the model proposes, code disposes.
88
*
99
* To add a provider in code: extend `LlmProvider`, add a `DEFAULT_MODEL` entry, teach `pickProvider()`
@@ -27,6 +27,18 @@ const DEFAULT_MODEL: Record<LlmProvider, string> = {
2727
venice: 'llama-3.3-70b',
2828
}
2929

30+
const KIMI_MIN_COMPLETION_TOKENS = 1024
31+
32+
/**
33+
* Venice-hosted Kimi code models can spend small budgets on internal reasoning before emitting
34+
* `message.content`. Keep caller limits for other models, but give Kimi enough room to finish JSON.
35+
*/
36+
export function effectiveMaxTokens(provider: LlmProvider, model: string, requested: number): number {
37+
return provider === 'venice' && /kimi/i.test(model) && requested < KIMI_MIN_COMPLETION_TOKENS
38+
? KIMI_MIN_COMPLETION_TOKENS
39+
: requested
40+
}
41+
3042
export interface CompleteOpts {
3143
system: string
3244
user: string
@@ -43,9 +55,10 @@ export async function complete(opts: CompleteOpts): Promise<string> {
4355
const provider = pickProvider()
4456
// `||` not `??`: coral manifests default unset options to "" — an empty LLM_MODEL must not win.
4557
const model = opts.model || process.env.LLM_MODEL || DEFAULT_MODEL[provider]
46-
const maxTokens = opts.maxTokens ?? 512
58+
const requestedMaxTokens = opts.maxTokens ?? 512
59+
const maxTokens = effectiveMaxTokens(provider, model, requestedMaxTokens)
4760
const trace = process.env.TRACE === '1'
48-
if (trace) console.error(`[llm] provider=${provider} model=${model}`)
61+
if (trace) console.error(`[llm] provider=${provider} model=${model} maxTokens=${maxTokens}`)
4962

5063
const text = provider === 'openai'
5164
? await completeOpenAI(opts, model, maxTokens)

0 commit comments

Comments
 (0)