32f5f857d2
The hosted-API BYOK fallback was hard-wired to api.anthropic.com. Issue #16 asks for Azure (Microsoft OpenAI hosting Anthropic-style models) but the same plumbing unlocks every common BYOK target — OpenRouter, LiteLLM, DeepSeek, Groq, Together, Mistral, plus Google Gemini direct. Provider model: - New `provider: 'anthropic' | 'openai' | 'azure' | 'google'` discriminator on AppConfig (defaults to 'anthropic' so existing localStorage configs migrate seamlessly). - src/providers/model.ts routes to one of four streaming clients: anthropic.ts (existing SDK path), openai.ts (new SSE pump shared with azure.ts), azure.ts (deployment URL + api-key header), google.ts (Generative Language streamGenerateContent). - src/providers/presets.ts ships per-provider defaults (baseUrl, model suggestions, api-key placeholder, Azure api-version flag) so the UI can stay declarative. UI: - SettingsDialog now shows a provider picker on the Hosted-API tab and surfaces an Azure-only api-version field. Provider switches preserve any non-empty user values. - EntryView / AvatarMenu env meta line shows the active provider label. - en + zh-CN locales updated; README + README.zh-CN document every provider, with explicit guidance to reach AWS Bedrock / GCP Vertex Anthropic models via a server-side LiteLLM proxy (signing belongs on the server, not the browser). Why an OpenAI-compatible adapter rather than a native Bedrock/Vertex client: AWS SigV4 and GCP service-account JWTs aren't safe to do from a browser holding long-lived BYOK credentials. LiteLLM (or any Anthropic/OpenAI-compatible proxy) sidesteps that and is the same path lobe-chat uses for Bedrock/Vertex.
235 lines
8.2 KiB
TypeScript
235 lines
8.2 KiB
TypeScript
export type ExecMode = 'daemon' | 'api';
|
|
|
|
// Which BYOK model endpoint to talk to in `mode === 'api'`. Each provider
|
|
// has its own request shape — see src/providers/{anthropic,openai,azure,
|
|
// google}.ts for the wire details. AWS Bedrock and Google Vertex are
|
|
// reached via the `anthropic` provider pointed at an Anthropic-compatible
|
|
// proxy (e.g. LiteLLM), which keeps signing on the server where the
|
|
// long-lived AWS / GCP credentials belong.
|
|
export type ModelProvider = 'anthropic' | 'openai' | 'azure' | 'google';
|
|
|
|
export interface AppConfig {
|
|
mode: ExecMode;
|
|
// Active provider when `mode === 'api'`. Older configs that predate the
|
|
// multi-provider rework default to 'anthropic' on load.
|
|
provider: ModelProvider;
|
|
apiKey: string;
|
|
baseUrl: string;
|
|
model: string;
|
|
// Azure OpenAI only — the api-version query string the Azure REST
|
|
// surface requires (e.g. '2024-08-01-preview'). Ignored by every other
|
|
// provider so the same config can round-trip through localStorage.
|
|
apiVersion?: string;
|
|
agentId: string | null;
|
|
skillId: string | null;
|
|
designSystemId: string | null;
|
|
// True once the user has been through the welcome onboarding modal at
|
|
// least once (saved or skipped). Bootstrap skips the auto-popup when
|
|
// this is set so refreshing the page doesn't re-prompt.
|
|
onboardingCompleted?: boolean;
|
|
}
|
|
|
|
export type AgentEvent =
|
|
| { kind: 'status'; label: string; detail?: string | undefined }
|
|
| { kind: 'text'; text: string }
|
|
| { kind: 'thinking'; text: string }
|
|
| { kind: 'tool_use'; id: string; name: string; input: unknown }
|
|
| { kind: 'tool_result'; toolUseId: string; content: string; isError: boolean }
|
|
| { kind: 'usage'; inputTokens?: number; outputTokens?: number; costUsd?: number; durationMs?: number }
|
|
| { kind: 'raw'; line: string };
|
|
|
|
export interface ChatMessage {
|
|
id: string;
|
|
role: 'user' | 'assistant';
|
|
content: string;
|
|
events?: AgentEvent[];
|
|
startedAt?: number;
|
|
endedAt?: number;
|
|
// Files staged by the user on this turn (uploaded into the project
|
|
// folder). Persisted on the message so re-renders show the same chips.
|
|
attachments?: ChatAttachment[];
|
|
// Files that appeared in the project folder during this assistant turn.
|
|
// Rendered as download / open chips at the end of the message so the
|
|
// user can grab a generated artifact (.pptx, .zip, etc.) in one click.
|
|
producedFiles?: ProjectFile[];
|
|
}
|
|
|
|
// Reference to a file that lives in the active project folder. The user
|
|
// stages these by paste / drop / picker / @-mention; the daemon receives
|
|
// `path` on the chat call and the agent reads them from cwd.
|
|
export interface ChatAttachment {
|
|
path: string;
|
|
name: string;
|
|
kind: 'image' | 'file';
|
|
size?: number;
|
|
}
|
|
|
|
export interface Artifact {
|
|
identifier: string;
|
|
title: string;
|
|
html: string;
|
|
savedUrl?: string;
|
|
}
|
|
|
|
export interface ExamplePreview {
|
|
source: 'skill' | 'design-system';
|
|
id: string;
|
|
title: string;
|
|
html: string;
|
|
}
|
|
|
|
export interface AgentInfo {
|
|
id: string;
|
|
name: string;
|
|
bin: string;
|
|
available: boolean;
|
|
path?: string;
|
|
version?: string | null;
|
|
}
|
|
|
|
export interface SkillSummary {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
triggers: string[];
|
|
mode: 'prototype' | 'deck' | 'template' | 'design-system';
|
|
platform?: 'desktop' | 'mobile' | null;
|
|
scenario?: string | null;
|
|
previewType: string;
|
|
designSystemRequired: boolean;
|
|
defaultFor: string[];
|
|
upstream: string | null;
|
|
/** Lower number = higher priority in the Examples gallery. `null` keeps
|
|
* the skill in its natural alphabetical position below all featured
|
|
* entries. Set via `od.featured` in the SKILL.md frontmatter. */
|
|
featured?: number | null;
|
|
/** Optional metadata hints, parsed from `od.fidelity`,
|
|
* `od.speaker_notes`, and `od.animations` in SKILL.md. Used by the
|
|
* Examples gallery's "Use this prompt" fast-create path to mirror the
|
|
* shipped `example.html` (e.g. wireframe-sketch declares
|
|
* `fidelity: wireframe`). Missing hints fall back to the same defaults
|
|
* the new-project form would apply. */
|
|
fidelity?: 'wireframe' | 'high-fidelity' | null;
|
|
speakerNotes?: boolean | null;
|
|
animations?: boolean | null;
|
|
hasBody: boolean;
|
|
examplePrompt: string;
|
|
}
|
|
|
|
export interface SkillDetail extends SkillSummary {
|
|
body: string;
|
|
}
|
|
|
|
export interface DesignSystemSummary {
|
|
id: string;
|
|
title: string;
|
|
category: string;
|
|
summary: string;
|
|
/** 4 representative hex strings extracted from DESIGN.md: [bg, support, fg, accent].
|
|
* Empty when DESIGN.md doesn't expose its tokens in the bold-and-hex format. */
|
|
swatches?: string[];
|
|
}
|
|
|
|
export interface DesignSystemDetail extends DesignSystemSummary {
|
|
body: string;
|
|
}
|
|
|
|
export type ProjectFileKind =
|
|
| 'html'
|
|
| 'image'
|
|
| 'sketch'
|
|
| 'text'
|
|
| 'code'
|
|
| 'binary';
|
|
|
|
export interface ProjectFile {
|
|
name: string;
|
|
// Project-relative path. Today the project folder is flat so `path`
|
|
// equals `name` for every file — but components that want to think in
|
|
// path terms (the @-mention picker, the staged-attachment chips) can
|
|
// read this without caring whether subdirs exist.
|
|
path?: string;
|
|
// Discriminator for code that wants to filter files vs dirs in a tree
|
|
// listing. The current listing is files-only; we always set this to
|
|
// 'file' so the discriminator is meaningful.
|
|
type?: 'file' | 'dir';
|
|
size: number;
|
|
mtime: number;
|
|
kind: ProjectFileKind;
|
|
mime: string;
|
|
}
|
|
|
|
// Per-project metadata captured at creation time. The agent reads this
|
|
// during chat (via the system prompt) and the question-form re-asks for
|
|
// any field that's missing. Each `kind` carries a different shape.
|
|
export type ProjectKind = 'prototype' | 'deck' | 'template' | 'other';
|
|
|
|
export interface ProjectMetadata {
|
|
kind: ProjectKind;
|
|
// Prototype: 'wireframe' | 'high-fidelity'. Drives the visual ambition.
|
|
fidelity?: 'wireframe' | 'high-fidelity';
|
|
// Slide deck: whether the user wants speaker notes (less text per slide).
|
|
speakerNotes?: boolean;
|
|
// Template: whether motion/animation should be part of the design.
|
|
// Defaults `false` so a static template stays static unless asked.
|
|
animations?: boolean;
|
|
// Template: id of the user-saved template chosen at creation time.
|
|
// Only set on `kind === 'template'` projects (the other kinds dropped
|
|
// template selection entirely). The built-in 'animation' starter no
|
|
// longer ships — every template here is user-created via Share menu.
|
|
templateId?: string;
|
|
// Template: human-readable label of the source template, kept separate
|
|
// from `templateId` so the agent surface can name it without re-fetching.
|
|
templateLabel?: string;
|
|
// Multi-select design-system "inspirations". The first pick still goes to
|
|
// `Project.designSystemId` (the primary system that controls tokens); any
|
|
// additional ids land here and are passed to the agent as references the
|
|
// generated artifact should *also* draw from. Empty / undefined when the
|
|
// user stayed in single-select mode.
|
|
inspirationDesignSystemIds?: string[];
|
|
}
|
|
|
|
export interface Project {
|
|
id: string;
|
|
name: string;
|
|
skillId: string | null;
|
|
designSystemId: string | null;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
// The prompt that should be prefilled into the chat composer when the
|
|
// project is opened. Cleared the first time the project is opened so it
|
|
// doesn't keep re-populating on subsequent visits.
|
|
pendingPrompt?: string;
|
|
// Optional structured metadata captured by the new-project panel. The
|
|
// shape varies by `kind`. Older projects created before this field
|
|
// existed will have it `undefined`.
|
|
metadata?: ProjectMetadata;
|
|
}
|
|
|
|
export interface ProjectTemplate {
|
|
id: string;
|
|
name: string;
|
|
// Source project the template was captured from (so we can show "based
|
|
// on …" in the picker). Optional because some templates are seeded.
|
|
sourceProjectId?: string;
|
|
// Snapshot of HTML files at the moment the template was saved. Each
|
|
// entry is a basename → text content pair.
|
|
files: Array<{ name: string; content: string }>;
|
|
// Free-form description shown in the picker.
|
|
description?: string;
|
|
createdAt: number;
|
|
}
|
|
|
|
export interface Conversation {
|
|
id: string;
|
|
projectId: string;
|
|
title: string | null;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
export interface OpenTabsState {
|
|
tabs: string[];
|
|
active: string | null;
|
|
}
|