mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
* feat: refactor Claude API Profiles to Claude Compatible Providers
- Rename ClaudeApiProfile to ClaudeCompatibleProvider with models[] array
- Each ProviderModel has mapsToClaudeModel field for Claude tier mapping
- Add providerType field for provider-specific icons (glm, minimax, openrouter)
- Add thinking level support for provider models in phase selectors
- Show all mapped Claude models per provider model (e.g., "Maps to Haiku, Sonnet, Opus")
- Add Bulk Replace feature to switch all phases to a provider at once
- Hide Bulk Replace button when no providers are enabled
- Fix project-level phaseModelOverrides not persisting after refresh
- Fix deleting last provider not persisting (remove empty array guard)
- Add getProviderByModelId() helper for all SDK routes
- Update all routes to pass provider config for provider models
- Update terminology from "profiles" to "providers" throughout UI
- Update documentation to reflect new provider system
* fix: atomic writer race condition and bulk replace reset to defaults
1. AtomicWriter Race Condition Fix (libs/utils/src/atomic-writer.ts):
- Changed temp file naming from Date.now() to Date.now() + random hex
- Uses crypto.randomBytes(4).toString('hex') for uniqueness
- Prevents ENOENT errors when multiple concurrent writes happen
within the same millisecond
2. Bulk Replace "Anthropic Direct" Reset (both dialogs):
- When selecting "Anthropic Direct", now uses DEFAULT_PHASE_MODELS
- Properly resets thinking levels and other settings to defaults
- Added thinkingLevel to the change detection comparison
- Affects both global and project-level bulk replace dialogs
* fix: update tests for new model resolver passthrough behavior
1. model-resolver tests:
- Unknown models now pass through unchanged (provider model support)
- Removed expectations for warnings on unknown models
- Updated case sensitivity and edge case tests accordingly
- Added tests for provider-like model names (GLM-4.7, MiniMax-M2.1)
2. atomic-writer tests:
- Updated regex to match new temp file format with random suffix
- Format changed from .tmp.{timestamp} to .tmp.{timestamp}.{hex}
* refactor: simplify getPhaseModelWithOverrides calls per code review
Address code review feedback on PR #629:
- Make settingsService parameter optional in getPhaseModelWithOverrides
- Function now handles undefined settingsService gracefully by returning defaults
- Remove redundant ternary checks in 4 call sites:
- apps/server/src/routes/context/routes/describe-file.ts
- apps/server/src/routes/context/routes/describe-image.ts
- apps/server/src/routes/worktree/routes/generate-commit-message.ts
- apps/server/src/services/auto-mode-service.ts
- Remove unused DEFAULT_PHASE_MODELS imports where applicable
* test: fix server tests for provider model passthrough behavior
- Update model-resolver.test.ts to expect unknown models to pass through
unchanged (supports ClaudeCompatibleProvider models like GLM-4.7)
- Remove warning expectations for unknown models (valid for providers)
- Add missing getCredentials and getGlobalSettings mocks to
ideation-service.test.ts for settingsService
* fix: address code review feedback for model providers
- Honor thinkingLevel in generate-commit-message.ts
- Pass claudeCompatibleProvider in ideation-service.ts for provider models
- Resolve provider configuration for model overrides in generate-suggestions.ts
- Update "Active Profile" to "Active Provider" label in project-claude-section
- Use substring instead of deprecated substr in api-profiles-section
- Preserve provider enabled state when editing in api-profiles-section
* fix: address CodeRabbit review issues for Claude Compatible Providers
- Fix TypeScript TS2339 error in generate-suggestions.ts where
settingsService was narrowed to 'never' type in else branch
- Use DEFAULT_PHASE_MODELS per-phase defaults instead of hardcoded
'sonnet' in settings-helpers.ts
- Remove duplicate eventHooks key in use-settings-migration.ts
- Add claudeCompatibleProviders to localStorage migration parsing
and merging functions
- Handle canonical claude-* model IDs (claude-haiku, claude-sonnet,
claude-opus) in project-models-section display names
This resolves the CI build failures and addresses code review feedback.
* fix: skip broken list-view-priority E2E test and add Priority column label
- Skip list-view-priority.spec.ts with TODO explaining the infrastructure
issue: setupRealProject only sets localStorage but server settings
take precedence with localStorageMigrated: true
- Add 'Priority' label to list-header.tsx for the priority column
(was empty string, now shows proper header text)
- Increase column width to accommodate the label
The E2E test issue is that tests create features in a temp directory,
but the server loads from the E2E Test Project fixture path set in
setup-e2e-fixtures.mjs. Needs infrastructure fix to properly switch
projects or create features through UI instead of on disk.
276 lines
9.5 KiB
TypeScript
276 lines
9.5 KiB
TypeScript
/**
|
|
* Simple Query Service - Simplified interface for basic AI queries
|
|
*
|
|
* Use this for routes that need simple text responses without
|
|
* complex event handling. This service abstracts away the provider
|
|
* selection and streaming details, providing a clean interface
|
|
* for common query patterns.
|
|
*
|
|
* Benefits:
|
|
* - No direct SDK imports needed in route files
|
|
* - Consistent provider routing based on model
|
|
* - Automatic text extraction from streaming responses
|
|
* - Structured output support for JSON schema responses
|
|
* - Eliminates duplicate extractTextFromStream() functions
|
|
*/
|
|
|
|
import { ProviderFactory } from './provider-factory.js';
|
|
import type {
|
|
ProviderMessage,
|
|
ContentBlock,
|
|
ThinkingLevel,
|
|
ReasoningEffort,
|
|
ClaudeApiProfile,
|
|
ClaudeCompatibleProvider,
|
|
Credentials,
|
|
} from '@automaker/types';
|
|
import { stripProviderPrefix } from '@automaker/types';
|
|
|
|
/**
|
|
* Options for simple query execution
|
|
*/
|
|
export interface SimpleQueryOptions {
|
|
/** The prompt to send to the AI (can be text or multi-part content) */
|
|
prompt: string | Array<{ type: string; text?: string; source?: object }>;
|
|
/** Model to use (with or without provider prefix) */
|
|
model?: string;
|
|
/** Working directory for the query */
|
|
cwd: string;
|
|
/** System prompt (combined with user prompt for some providers) */
|
|
systemPrompt?: string;
|
|
/** Maximum turns for agentic operations (default: 1) */
|
|
maxTurns?: number;
|
|
/** Tools to allow (default: [] for simple queries) */
|
|
allowedTools?: string[];
|
|
/** Abort controller for cancellation */
|
|
abortController?: AbortController;
|
|
/** Structured output format for JSON responses */
|
|
outputFormat?: {
|
|
type: 'json_schema';
|
|
schema: Record<string, unknown>;
|
|
};
|
|
/** Thinking level for Claude models */
|
|
thinkingLevel?: ThinkingLevel;
|
|
/** Reasoning effort for Codex/OpenAI models */
|
|
reasoningEffort?: ReasoningEffort;
|
|
/** If true, runs in read-only mode (no file writes) */
|
|
readOnly?: boolean;
|
|
/** Setting sources for CLAUDE.md loading */
|
|
settingSources?: Array<'user' | 'project' | 'local'>;
|
|
/**
|
|
* Active Claude API profile for alternative endpoint configuration
|
|
* @deprecated Use claudeCompatibleProvider instead
|
|
*/
|
|
claudeApiProfile?: ClaudeApiProfile;
|
|
/**
|
|
* Claude-compatible provider for alternative endpoint configuration.
|
|
* Takes precedence over claudeApiProfile if both are set.
|
|
*/
|
|
claudeCompatibleProvider?: ClaudeCompatibleProvider;
|
|
/** Credentials for resolving 'credentials' apiKeySource in Claude API profiles/providers */
|
|
credentials?: Credentials;
|
|
}
|
|
|
|
/**
|
|
* Result from a simple query
|
|
*/
|
|
export interface SimpleQueryResult {
|
|
/** The accumulated text response */
|
|
text: string;
|
|
/** Structured output if outputFormat was specified and provider supports it */
|
|
structured_output?: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* Options for streaming query execution
|
|
*/
|
|
export interface StreamingQueryOptions extends SimpleQueryOptions {
|
|
/** Callback for each text chunk received */
|
|
onText?: (text: string) => void;
|
|
/** Callback for tool use events */
|
|
onToolUse?: (tool: string, input: unknown) => void;
|
|
/** Callback for thinking blocks (if available) */
|
|
onThinking?: (thinking: string) => void;
|
|
}
|
|
|
|
/**
|
|
* Default model to use when none specified
|
|
*/
|
|
const DEFAULT_MODEL = 'claude-sonnet-4-20250514';
|
|
|
|
/**
|
|
* Execute a simple query and return the text result
|
|
*
|
|
* Use this for simple, non-streaming queries where you just need
|
|
* the final text response. For more complex use cases with progress
|
|
* callbacks, use streamingQuery() instead.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const result = await simpleQuery({
|
|
* prompt: 'Generate a title for: user authentication',
|
|
* cwd: process.cwd(),
|
|
* systemPrompt: 'You are a title generator...',
|
|
* maxTurns: 1,
|
|
* allowedTools: [],
|
|
* });
|
|
* console.log(result.text); // "Add user authentication"
|
|
* ```
|
|
*/
|
|
export async function simpleQuery(options: SimpleQueryOptions): Promise<SimpleQueryResult> {
|
|
const model = options.model || DEFAULT_MODEL;
|
|
const provider = ProviderFactory.getProviderForModel(model);
|
|
const bareModel = stripProviderPrefix(model);
|
|
|
|
let responseText = '';
|
|
let structuredOutput: Record<string, unknown> | undefined;
|
|
|
|
// Build provider options
|
|
const providerOptions = {
|
|
prompt: options.prompt,
|
|
model: bareModel,
|
|
originalModel: model,
|
|
cwd: options.cwd,
|
|
systemPrompt: options.systemPrompt,
|
|
maxTurns: options.maxTurns ?? 1,
|
|
allowedTools: options.allowedTools ?? [],
|
|
abortController: options.abortController,
|
|
outputFormat: options.outputFormat,
|
|
thinkingLevel: options.thinkingLevel,
|
|
reasoningEffort: options.reasoningEffort,
|
|
readOnly: options.readOnly,
|
|
settingSources: options.settingSources,
|
|
claudeApiProfile: options.claudeApiProfile, // Legacy: Pass active Claude API profile for alternative endpoint configuration
|
|
claudeCompatibleProvider: options.claudeCompatibleProvider, // New: Pass Claude-compatible provider (takes precedence)
|
|
credentials: options.credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
|
};
|
|
|
|
for await (const msg of provider.executeQuery(providerOptions)) {
|
|
// Handle error messages
|
|
if (msg.type === 'error') {
|
|
const errorMessage = msg.error || 'Provider returned an error';
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
// Extract text from assistant messages
|
|
if (msg.type === 'assistant' && msg.message?.content) {
|
|
for (const block of msg.message.content) {
|
|
if (block.type === 'text' && block.text) {
|
|
responseText += block.text;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle result messages
|
|
if (msg.type === 'result') {
|
|
if (msg.subtype === 'success') {
|
|
// Use result text if longer than accumulated text
|
|
if (msg.result && msg.result.length > responseText.length) {
|
|
responseText = msg.result;
|
|
}
|
|
// Capture structured output if present
|
|
if (msg.structured_output) {
|
|
structuredOutput = msg.structured_output;
|
|
}
|
|
} else if (msg.subtype === 'error_max_turns') {
|
|
// Max turns reached - return what we have
|
|
break;
|
|
} else if (msg.subtype === 'error_max_structured_output_retries') {
|
|
throw new Error('Could not produce valid structured output after retries');
|
|
}
|
|
}
|
|
}
|
|
|
|
return { text: responseText, structured_output: structuredOutput };
|
|
}
|
|
|
|
/**
|
|
* Execute a streaming query with event callbacks
|
|
*
|
|
* Use this for queries where you need real-time progress updates,
|
|
* such as when displaying streaming output to a user.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const result = await streamingQuery({
|
|
* prompt: 'Analyze this project and suggest improvements',
|
|
* cwd: '/path/to/project',
|
|
* maxTurns: 250,
|
|
* allowedTools: ['Read', 'Glob', 'Grep'],
|
|
* onText: (text) => emitProgress(text),
|
|
* onToolUse: (tool, input) => emitToolUse(tool, input),
|
|
* });
|
|
* ```
|
|
*/
|
|
export async function streamingQuery(options: StreamingQueryOptions): Promise<SimpleQueryResult> {
|
|
const model = options.model || DEFAULT_MODEL;
|
|
const provider = ProviderFactory.getProviderForModel(model);
|
|
const bareModel = stripProviderPrefix(model);
|
|
|
|
let responseText = '';
|
|
let structuredOutput: Record<string, unknown> | undefined;
|
|
|
|
// Build provider options
|
|
const providerOptions = {
|
|
prompt: options.prompt,
|
|
model: bareModel,
|
|
originalModel: model,
|
|
cwd: options.cwd,
|
|
systemPrompt: options.systemPrompt,
|
|
maxTurns: options.maxTurns ?? 250,
|
|
allowedTools: options.allowedTools ?? ['Read', 'Glob', 'Grep'],
|
|
abortController: options.abortController,
|
|
outputFormat: options.outputFormat,
|
|
thinkingLevel: options.thinkingLevel,
|
|
reasoningEffort: options.reasoningEffort,
|
|
readOnly: options.readOnly,
|
|
settingSources: options.settingSources,
|
|
claudeApiProfile: options.claudeApiProfile, // Legacy: Pass active Claude API profile for alternative endpoint configuration
|
|
claudeCompatibleProvider: options.claudeCompatibleProvider, // New: Pass Claude-compatible provider (takes precedence)
|
|
credentials: options.credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
|
};
|
|
|
|
for await (const msg of provider.executeQuery(providerOptions)) {
|
|
// Handle error messages
|
|
if (msg.type === 'error') {
|
|
const errorMessage = msg.error || 'Provider returned an error';
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
// Extract content from assistant messages
|
|
if (msg.type === 'assistant' && msg.message?.content) {
|
|
for (const block of msg.message.content) {
|
|
if (block.type === 'text' && block.text) {
|
|
responseText += block.text;
|
|
options.onText?.(block.text);
|
|
} else if (block.type === 'tool_use' && block.name) {
|
|
options.onToolUse?.(block.name, block.input);
|
|
} else if (block.type === 'thinking' && block.thinking) {
|
|
options.onThinking?.(block.thinking);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle result messages
|
|
if (msg.type === 'result') {
|
|
if (msg.subtype === 'success') {
|
|
// Use result text if longer than accumulated text
|
|
if (msg.result && msg.result.length > responseText.length) {
|
|
responseText = msg.result;
|
|
}
|
|
// Capture structured output if present
|
|
if (msg.structured_output) {
|
|
structuredOutput = msg.structured_output;
|
|
}
|
|
} else if (msg.subtype === 'error_max_turns') {
|
|
// Max turns reached - return what we have
|
|
break;
|
|
} else if (msg.subtype === 'error_max_structured_output_retries') {
|
|
throw new Error('Could not produce valid structured output after retries');
|
|
}
|
|
}
|
|
}
|
|
|
|
return { text: responseText, structured_output: structuredOutput };
|
|
}
|