Files
open-design/src/types.ts
T
pftom 32f5f857d2 feat(providers): support OpenAI-compatible / Azure / Google Gemini endpoints (closes #16)
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.
2026-04-29 07:44:00 +08:00

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;
}