Skip to content

Commit d4433b0

Browse files
V48 Gate 3 (implementation-only): xAI Grok default provider + no empty telemetry for anchors
- Add xai provider (OpenAI-compatible https://api.x.ai/v1, default model grok-4.5) - Prefer XAI_API_KEY for default provider; setDefaultProvider on registry factory - Exclude activity-ledger anchors from Deposit pipelines table; show ledger detail instead of empty pipeline telemetry when an anchor URL is opened - Document XAI_API_KEY in .env.example (secrets stay in .env.local only)
1 parent 56c5b22 commit d4433b0

8 files changed

Lines changed: 225 additions & 3 deletions

File tree

packages/generic-llms/src/defaults.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ export function resolveDefaultLLMProvider(env: BitcodeLLMEnvironment = process.e
44
const configured = normalizeEnvValue(env.BITCODE_LLM_PROVIDER);
55
if (configured) return configured.toLowerCase();
66

7+
// Prefer xAI / Grok when a key is present (product default when configured).
8+
if (normalizeEnvValue(env.XAI_API_KEY) || normalizeEnvValue(env.GROK_API_KEY)) {
9+
return 'xai';
10+
}
711
if (normalizeEnvValue(env.OPENAI_API_KEY)) return 'openai';
812
if (normalizeEnvValue(env.ANTHROPIC_API_KEY)) return 'anthropic';
913
if (
@@ -25,6 +29,9 @@ export function resolveDefaultLLMModel(
2529
if (configured) return configured;
2630

2731
switch (provider.toLowerCase()) {
32+
case 'xai':
33+
case 'grok':
34+
return 'grok-4.5';
2835
case 'google':
2936
return 'gemini-2.5-flash';
3037
case 'anthropic':

packages/generic-llms/src/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
export { openAIProvider } from './providers/openai';
88
export { anthropicProvider } from './providers/anthropic';
99
export { googleProvider } from './providers/google';
10+
export { xaiProvider } from './providers/xai';
1011
export { resolveDefaultLLMConfig, resolveDefaultLLMModel, resolveDefaultLLMProvider, type BitcodeLLMEnvironment, } from './defaults';
1112
import { LLMRegistry } from '@bitcode/llm-generics';
1213
export declare function factoryLLMRegistryWithProviders(): LLMRegistry;

packages/generic-llms/src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
export { openAIProvider } from './providers/openai';
1010
export { anthropicProvider } from './providers/anthropic';
1111
export { googleProvider } from './providers/google';
12+
export { xaiProvider } from './providers/xai';
1213
export {
1314
resolveDefaultLLMConfig,
1415
resolveDefaultLLMModel,
@@ -18,6 +19,7 @@ export {
1819

1920
// Factory for pre-configured registry
2021
import { factoryLLMRegistry, LLMRegistry } from '@bitcode/llm-generics';
22+
import { resolveDefaultLLMConfig } from './defaults';
2123

2224
export function factoryLLMRegistryWithProviders(): LLMRegistry {
2325
const registry = factoryLLMRegistry();
@@ -26,11 +28,18 @@ export function factoryLLMRegistryWithProviders(): LLMRegistry {
2628
const { openAIProvider } = require('./providers/openai');
2729
const { anthropicProvider } = require('./providers/anthropic');
2830
const { googleProvider } = require('./providers/google');
31+
const { xaiProvider } = require('./providers/xai');
2932

30-
// Register default providers
33+
// Register providers (xAI/Grok first so name is available for default)
34+
registry.registerProvider(xaiProvider);
3135
registry.registerProvider(openAIProvider);
3236
registry.registerProvider(anthropicProvider);
3337
registry.registerProvider(googleProvider);
38+
39+
// Apply environment-resolved default provider + model to the global path.
40+
const defaults = resolveDefaultLLMConfig();
41+
registry.setDefaultProvider(defaults.provider);
42+
registry.configure('*', { model: defaults.model }, 0);
3443

3544
return registry;
3645
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { LLMProvider } from '@bitcode/llm-generics';
2+
export declare const xaiProvider: LLMProvider;
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { LLMProvider, LLMConfig, LLMInput, LLMOutput } from '@bitcode/llm-generics';
2+
3+
/**
4+
* xAI / Grok provider — OpenAI-compatible client against https://api.x.ai/v1.
5+
*
6+
* Auth: XAI_API_KEY (preferred) or GROK_API_KEY.
7+
* Default model: grok-4.5 (overridable via BITCODE_LLM_MODEL).
8+
*
9+
* Uses chat.completions (OpenAI-compatible surface on xAI). The Responses API
10+
* is also available on the same base URL; chat is sufficient for Bitcode's
11+
* message-shaped LLMInput.
12+
*/
13+
14+
const XAI_BASE_URL = 'https://api.x.ai/v1';
15+
const DEFAULT_XAI_MODEL = 'grok-4.5';
16+
17+
function resolveXaiApiKey(env: NodeJS.ProcessEnv = process.env): string | undefined {
18+
const key = env.XAI_API_KEY || env.GROK_API_KEY;
19+
return typeof key === 'string' && key.trim() ? key.trim() : undefined;
20+
}
21+
22+
export const xaiProvider: LLMProvider = {
23+
name: 'xai',
24+
25+
createLLM(config: LLMConfig) {
26+
return async (input: LLMInput): Promise<LLMOutput> => {
27+
const finalConfig = { ...config, ...input.config };
28+
const model = finalConfig.model || DEFAULT_XAI_MODEL;
29+
30+
try {
31+
// eslint-disable-next-line @typescript-eslint/no-var-requires
32+
const OpenAI = require('openai');
33+
const apiKey = resolveXaiApiKey();
34+
if (!apiKey) {
35+
throw new Error('XAI_API_KEY is not set');
36+
}
37+
const client = new OpenAI({
38+
apiKey,
39+
baseURL: XAI_BASE_URL,
40+
});
41+
const sys = (input.messages || [])
42+
.filter((m) => m.role === 'system')
43+
.map((m) => ({ role: 'system' as const, content: m.content }));
44+
const nonSys = (input.messages || [])
45+
.filter((m) => m.role !== 'system')
46+
.map((m) => ({
47+
role: (m.role === 'assistant' ? 'assistant' : 'user') as 'assistant' | 'user',
48+
content: m.content,
49+
}));
50+
const messages = [...sys, ...nonSys];
51+
const resp = await client.chat.completions.create({
52+
model,
53+
messages: messages as any,
54+
temperature: finalConfig.temperature,
55+
max_tokens: finalConfig.maxTokens,
56+
top_p: finalConfig.topP,
57+
frequency_penalty: finalConfig.frequencyPenalty,
58+
presence_penalty: finalConfig.presencePenalty,
59+
stop: finalConfig.stopSequences,
60+
response_format:
61+
finalConfig.responseFormat === 'json'
62+
? { type: 'json_object' }
63+
: undefined,
64+
seed: finalConfig.seed,
65+
});
66+
const choice = resp.choices?.[0];
67+
const finish = choice?.finish_reason;
68+
const stopReason =
69+
finish === 'length'
70+
? 'length'
71+
: finish === 'stop'
72+
? 'stop'
73+
: finish || 'unknown';
74+
return {
75+
content: choice?.message?.content || '',
76+
usage: {
77+
inputTokens: resp.usage?.prompt_tokens || 0,
78+
outputTokens: resp.usage?.completion_tokens || 0,
79+
totalTokens: resp.usage?.total_tokens || 0,
80+
},
81+
metadata: {
82+
model: resp.model || model,
83+
provider: 'xai',
84+
finishReason: finish,
85+
stopReason,
86+
},
87+
};
88+
} catch (err) {
89+
const allowMock =
90+
process?.env?.BITCODE_LLM_ALLOW_MOCK === '1' ||
91+
process?.env?.NODE_ENV === 'test';
92+
if (!allowMock) {
93+
const hint =
94+
'Provide XAI_API_KEY and ensure the OpenAI SDK can reach https://api.x.ai/v1, or set BITCODE_LLM_ALLOW_MOCK=1 to permit mock.';
95+
const e = err instanceof Error ? err : new Error(String(err));
96+
(e as any).provider = 'xai';
97+
(e as any).model = model;
98+
throw new Error(`${e.message || 'xAI provider unavailable'}. ${hint}`);
99+
}
100+
const last = input.messages?.[input.messages.length - 1]?.content ?? '';
101+
return {
102+
content: `xAI Grok (mock) response to: ${last}`,
103+
usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
104+
metadata: { model, provider: 'xai', mocked: true },
105+
};
106+
}
107+
};
108+
},
109+
};

packages/generic-llms/tests/unit/registry.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,25 @@ describe('factoryLLMRegistryWithProviders', () => {
3838
});
3939
});
4040

41+
test('selects xAI / Grok as default when XAI_API_KEY is configured', () => {
42+
const defaults = resolveDefaultLLMConfig({
43+
XAI_API_KEY: 'xai-test',
44+
OPENAI_API_KEY: 'sk-test',
45+
ANTHROPIC_API_KEY: 'sk-ant-test',
46+
});
47+
48+
expect(defaults).toEqual({
49+
provider: 'xai',
50+
model: 'grok-4.5',
51+
});
52+
});
53+
54+
test('registers xai provider for explicit selection', () => {
55+
const registry = factoryLLMRegistryWithProviders();
56+
const llm = registry.getLLM(['*'], 'xai');
57+
expect(typeof llm).toBe('function');
58+
});
59+
4160
test('preserves explicit provider and model overrides', () => {
4261
const defaults = resolveDefaultLLMConfig({
4362
OPENAI_API_KEY: 'sk-test',

uapi/.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ SUPABASE_JWT_SECRET=<your Supabase JWT secret>
2525

2626
# OpenAI API key for chat and other AI-driven features
2727
OPENAI_API_KEY=<your OpenAI secret key>
28+
# xAI / Grok (preferred default when set — OpenAI-compatible https://api.x.ai/v1)
29+
XAI_API_KEY=<your xAI API key>
30+
# Optional: force provider/model (defaults: xai when XAI_API_KEY is set, else openai/anthropic/google)
31+
# BITCODE_LLM_PROVIDER=xai
32+
# BITCODE_LLM_MODEL=grok-4.5
2833
BITCODE_ASSET_PACK_REAL_INFERENCE=false
2934
BITCODE_ASSET_PACK_REAL_INFERENCE_PROFILE=bounded
3035
BITCODE_ENABLE_PIPELINE_HARNESS_API=false

uapi/app/deposits/DepositPageClient.tsx

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,37 @@ export default function DepositPageClient() {
323323
// Detail owns the page when composing a new deposit OR viewing a run.
324324
const isDepositDetailOpen = Boolean(synthesisRunId) || isComposeOpen;
325325

326+
// Activity-ledger rows (Obfuscations / repository anchors) feed Load-anchor
327+
// dropdowns but are NOT pipeline executions — exclude them from the pipelines
328+
// table so selecting them cannot open empty "Telemetry / No logs" detail.
329+
const ACTIVITY_LEDGER_SOURCES = useMemo(
330+
() =>
331+
new Set([
332+
"deposit-obfuscations-anchor",
333+
"terminal-repository-context-panel",
334+
]),
335+
[],
336+
);
337+
const pipelineTableRuns = useMemo(
338+
() =>
339+
liveRuns.filter(
340+
(run) =>
341+
!run.contextSource || !ACTIVITY_LEDGER_SOURCES.has(run.contextSource),
342+
),
343+
[liveRuns, ACTIVITY_LEDGER_SOURCES],
344+
);
345+
const selectedDetailRun = useMemo(
346+
() =>
347+
synthesisRunId
348+
? liveRuns.find((run) => run.id === synthesisRunId) || null
349+
: null,
350+
[liveRuns, synthesisRunId],
351+
);
352+
const isActivityLedgerDetail = Boolean(
353+
selectedDetailRun?.contextSource &&
354+
ACTIVITY_LEDGER_SOURCES.has(selectedDetailRun.contextSource),
355+
);
356+
326357
const refreshLiveRuns = useCallback(async () => {
327358
setIsLoadingRuns(true);
328359
setRunsLoadError(null);
@@ -1597,7 +1628,7 @@ export default function DepositPageClient() {
15971628
{!isDepositDetailOpen ? (
15981629
<div className="mt-4" data-testid="deposits-pipelines-table">
15991630
<TerminalTransactionsTable
1600-
runs={liveRuns}
1631+
runs={pipelineTableRuns}
16011632
selectedTransactionId={selectedRun?.id ?? null}
16021633
onSelectTransaction={replaceDepositRouteTransaction}
16031634
filters={pipelineFilters}
@@ -1983,7 +2014,46 @@ export default function DepositPageClient() {
19832014
</section>
19842015
</div>
19852016

1986-
{synthesisRunId ? (
2017+
{synthesisRunId && isActivityLedgerDetail ? (
2018+
<section
2019+
className="min-w-0 overflow-hidden border border-white/10 bg-white/[0.035] px-4 py-4"
2020+
aria-label="Activity ledger record"
2021+
data-testid="deposit-activity-ledger-detail"
2022+
>
2023+
<p className="text-[0.68rem] uppercase tracking-[0.22em] text-emerald-200/80">
2024+
Activity ledger
2025+
</p>
2026+
<h2 className="mt-2 text-lg font-semibold text-white">
2027+
{selectedDetailRun?.contextSource ===
2028+
"deposit-obfuscations-anchor"
2029+
? "Obfuscations anchor"
2030+
: selectedDetailRun?.contextSource ===
2031+
"terminal-repository-context-panel"
2032+
? "Repository anchor"
2033+
: "Activity record"}
2034+
</h2>
2035+
<p className="mt-2 max-w-3xl text-sm leading-6 text-neutral-400">
2036+
This row is a saved configuration bookmark, not a pipeline
2037+
run. Pipeline telemetry (phases, agents, generations) only
2038+
appears for Asset Pack Synthesis executions. Use Load
2039+
anchor on a New deposit to apply this configuration, or Back
2040+
to return to the pipelines table.
2041+
</p>
2042+
{selectedDetailRun?.summary ? (
2043+
<p
2044+
className="mt-4 border border-white/10 bg-black/30 px-3 py-3 text-sm leading-6 text-neutral-200"
2045+
data-testid="deposit-activity-ledger-summary"
2046+
>
2047+
{selectedDetailRun.summary}
2048+
</p>
2049+
) : null}
2050+
<p className="mt-3 font-mono text-[0.62rem] text-neutral-500">
2051+
{synthesisRunId}
2052+
</p>
2053+
</section>
2054+
) : null}
2055+
2056+
{synthesisRunId && !isActivityLedgerDetail ? (
19872057
<section
19882058
ref={synthesisTelemetryRef}
19892059
className="min-w-0 overflow-hidden border border-white/10 bg-white/[0.035] px-4 py-4"

0 commit comments

Comments
 (0)