mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(web): clarify finalize BYOK requirements for Local CLI users (#3041)
Local CLI chat does not supply BYOK credentials to finalize synthesis. Resolve per-protocol saved settings before calling the daemon and show an actionable toast instead of a generic BAD_REQUEST when credentials are missing. Fixes #2959
This commit is contained in:
parent
f44a5d5816
commit
582a03195f
3 changed files with 170 additions and 12 deletions
|
|
@ -164,7 +164,10 @@ import { useTerminalLaunch } from '../hooks/useTerminalLaunch';
|
|||
import { buildContinueInCliToast } from '../lib/build-continue-in-cli-toast';
|
||||
import { buildClipboardPrompt } from '../lib/build-clipboard-prompt';
|
||||
import { copyToClipboard } from '../lib/copy-to-clipboard';
|
||||
import { effectiveMaxTokens } from '../state/maxTokens';
|
||||
import {
|
||||
buildFinalizeCredentialsMissingToast,
|
||||
buildFinalizeRequest,
|
||||
} from '../lib/resolve-finalize-request';
|
||||
|
||||
type ProjectChatSendMeta = ChatSendMeta & {
|
||||
retryOfAssistantId?: string;
|
||||
|
|
@ -3916,17 +3919,12 @@ export function ProjectView({
|
|||
// shortcut wiring. Close to the JSX so the data flow is easy to
|
||||
// trace from the toolbar back to its sources.
|
||||
const handleFinalize = useCallback(() => {
|
||||
const protocol = config.apiProtocol ?? 'anthropic';
|
||||
void finalize.trigger({
|
||||
protocol,
|
||||
apiKey: config.apiKey,
|
||||
baseUrl: config.baseUrl,
|
||||
model: config.model,
|
||||
maxTokens: effectiveMaxTokens(config),
|
||||
...(protocol === 'azure' && config.apiVersion?.trim()
|
||||
? { apiVersion: config.apiVersion.trim() }
|
||||
: {}),
|
||||
}).then((result) => {
|
||||
const request = buildFinalizeRequest(config);
|
||||
if (!request) {
|
||||
setProjectActionsToast(buildFinalizeCredentialsMissingToast(config));
|
||||
return;
|
||||
}
|
||||
void finalize.trigger(request).then((result) => {
|
||||
if (result) void designMdState.refresh();
|
||||
});
|
||||
}, [finalize, config, designMdState]);
|
||||
|
|
|
|||
81
apps/web/src/lib/resolve-finalize-request.ts
Normal file
81
apps/web/src/lib/resolve-finalize-request.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import type {
|
||||
FinalizeAnthropicRequest,
|
||||
FinalizeProviderProtocol,
|
||||
} from '@open-design/contracts';
|
||||
|
||||
import { effectiveMaxTokens } from '../state/maxTokens';
|
||||
import type { ApiProtocol, AppConfig } from '../types';
|
||||
|
||||
const FINALIZE_PROTOCOLS = new Set<FinalizeProviderProtocol>([
|
||||
'anthropic',
|
||||
'openai',
|
||||
'azure',
|
||||
'google',
|
||||
'ollama',
|
||||
]);
|
||||
|
||||
export interface FinalizeCredentialsMissingToast {
|
||||
message: string;
|
||||
details: string | null;
|
||||
}
|
||||
|
||||
function resolveFinalizeProtocol(config: AppConfig): FinalizeProviderProtocol {
|
||||
const protocol = config.apiProtocol ?? 'anthropic';
|
||||
return FINALIZE_PROTOCOLS.has(protocol as FinalizeProviderProtocol)
|
||||
? (protocol as FinalizeProviderProtocol)
|
||||
: 'anthropic';
|
||||
}
|
||||
|
||||
function resolveByokFields(config: AppConfig, protocol: ApiProtocol) {
|
||||
const saved = config.apiProtocolConfigs?.[protocol];
|
||||
return {
|
||||
apiKey: (saved?.apiKey ?? config.apiKey ?? '').trim(),
|
||||
baseUrl: (saved?.baseUrl ?? config.baseUrl ?? '').trim(),
|
||||
model: (saved?.model ?? config.model ?? '').trim(),
|
||||
apiVersion: (saved?.apiVersion ?? config.apiVersion ?? '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
export function isFinalizeByokConfigured(config: AppConfig): boolean {
|
||||
const protocol = resolveFinalizeProtocol(config);
|
||||
const { apiKey, model } = resolveByokFields(config, protocol);
|
||||
return Boolean(apiKey && model);
|
||||
}
|
||||
|
||||
export function buildFinalizeRequest(
|
||||
config: AppConfig,
|
||||
): FinalizeAnthropicRequest | null {
|
||||
const protocol = resolveFinalizeProtocol(config);
|
||||
const { apiKey, baseUrl, model, apiVersion } = resolveByokFields(
|
||||
config,
|
||||
protocol,
|
||||
);
|
||||
if (!apiKey || !model) return null;
|
||||
|
||||
return {
|
||||
protocol,
|
||||
apiKey,
|
||||
...(baseUrl ? { baseUrl } : {}),
|
||||
model,
|
||||
maxTokens: effectiveMaxTokens(config),
|
||||
...(protocol === 'azure' && apiVersion ? { apiVersion } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildFinalizeCredentialsMissingToast(
|
||||
config: AppConfig,
|
||||
): FinalizeCredentialsMissingToast {
|
||||
if (config.mode === 'daemon') {
|
||||
return {
|
||||
message:
|
||||
'Finalize design package needs BYOK API settings — Local CLI login is used for chat only.',
|
||||
details:
|
||||
'Open Settings → BYOK to add an API key and model, or use Continue in CLI (⌘⇧K) to finalize manually.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Bad request — check the API key and model.',
|
||||
details: 'Open Settings → BYOK and verify your API key, base URL, and model.',
|
||||
};
|
||||
}
|
||||
79
apps/web/tests/lib/resolve-finalize-request.test.ts
Normal file
79
apps/web/tests/lib/resolve-finalize-request.test.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
buildFinalizeCredentialsMissingToast,
|
||||
buildFinalizeRequest,
|
||||
isFinalizeByokConfigured,
|
||||
} from '../../src/lib/resolve-finalize-request';
|
||||
import { DEFAULT_CONFIG } from '../../src/state/config';
|
||||
|
||||
describe('resolve-finalize-request', () => {
|
||||
it('returns null when BYOK credentials are missing', () => {
|
||||
expect(
|
||||
buildFinalizeRequest({
|
||||
...DEFAULT_CONFIG,
|
||||
mode: 'daemon',
|
||||
apiKey: '',
|
||||
model: 'claude-sonnet-4-5',
|
||||
}),
|
||||
).toBeNull();
|
||||
expect(isFinalizeByokConfigured(DEFAULT_CONFIG)).toBe(false);
|
||||
});
|
||||
|
||||
it('resolves credentials from apiProtocolConfigs when top-level fields are empty', () => {
|
||||
const request = buildFinalizeRequest({
|
||||
...DEFAULT_CONFIG,
|
||||
mode: 'daemon',
|
||||
apiProtocol: 'google',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
model: '',
|
||||
apiProtocolConfigs: {
|
||||
google: {
|
||||
apiKey: 'google-key',
|
||||
baseUrl: 'https://generativelanguage.googleapis.com',
|
||||
model: 'gemini-2.5-pro',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(request).toMatchObject({
|
||||
protocol: 'google',
|
||||
apiKey: 'google-key',
|
||||
baseUrl: 'https://generativelanguage.googleapis.com',
|
||||
model: 'gemini-2.5-pro',
|
||||
});
|
||||
expect(isFinalizeByokConfigured({
|
||||
...DEFAULT_CONFIG,
|
||||
mode: 'daemon',
|
||||
apiProtocol: 'google',
|
||||
apiKey: '',
|
||||
model: '',
|
||||
apiProtocolConfigs: {
|
||||
google: {
|
||||
apiKey: 'google-key',
|
||||
baseUrl: 'https://generativelanguage.googleapis.com',
|
||||
model: 'gemini-2.5-pro',
|
||||
},
|
||||
},
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it('surfaces a Local CLI-specific toast when finalize lacks BYOK settings', () => {
|
||||
expect(
|
||||
buildFinalizeCredentialsMissingToast({
|
||||
...DEFAULT_CONFIG,
|
||||
mode: 'daemon',
|
||||
}).message,
|
||||
).toContain('Local CLI login is used for chat only');
|
||||
});
|
||||
|
||||
it('surfaces the generic BYOK toast when execution mode is api', () => {
|
||||
expect(
|
||||
buildFinalizeCredentialsMissingToast({
|
||||
...DEFAULT_CONFIG,
|
||||
mode: 'api',
|
||||
}).message,
|
||||
).toBe('Bad request — check the API key and model.');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue