mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-16 16:02:33 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e694a6a045 | ||
|
|
7ce8e4f581 | ||
|
|
90d82b2e84 | ||
|
|
7a9c7687f7 | ||
|
|
54e1862909 |
@@ -416,6 +416,8 @@ class AIChatManager {
|
||||
// on each iteration. This is critical for changeModeTool (Navigator → Script/Flow)
|
||||
// which reassigns this.tools, this.helpers, this.systemMessage mid-loop.
|
||||
const self = this
|
||||
// Pass promise-holders rather than awaited clients so a construction
|
||||
// failure in the unused provider doesn't block a chat on the other.
|
||||
const result = await runChatLoop({
|
||||
messages,
|
||||
get systemMessage() {
|
||||
@@ -433,8 +435,12 @@ class AIChatManager {
|
||||
return getCurrentModel()
|
||||
},
|
||||
clients: {
|
||||
openai: workspaceAIClients.getOpenaiClient(),
|
||||
anthropic: workspaceAIClients.getAnthropicClient()
|
||||
get openai() {
|
||||
return workspaceAIClients.getOpenaiClient()
|
||||
},
|
||||
get anthropic() {
|
||||
return workspaceAIClients.getAnthropicClient()
|
||||
}
|
||||
},
|
||||
workspace: get(workspaceStore) ?? '',
|
||||
skipResponsesApi: this.skipResponsesApi,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OpenAI } from 'openai'
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
import type { OpenAI } from 'openai'
|
||||
import type Anthropic from '@anthropic-ai/sdk'
|
||||
import type {
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionMessageFunctionToolCall
|
||||
@@ -30,7 +30,7 @@ export async function getAnthropicCompletion(
|
||||
tools?: OpenAI.Chat.Completions.ChatCompletionFunctionTool[],
|
||||
options?: {
|
||||
forceModelProvider?: AIProviderModel
|
||||
anthropicClient?: Anthropic
|
||||
anthropicClient?: Anthropic | Promise<Anthropic>
|
||||
}
|
||||
): Promise<MessageStream> {
|
||||
const { provider, config } = getProviderAndCompletionConfig({
|
||||
@@ -41,7 +41,7 @@ export async function getAnthropicCompletion(
|
||||
const { system, messages: anthropicMessages } = convertOpenAIToAnthropicMessages(messages)
|
||||
const anthropicTools = convertOpenAIToolsToAnthropic(tools)
|
||||
|
||||
const client = options?.anthropicClient ?? workspaceAIClients.getAnthropicClient()
|
||||
const client = await (options?.anthropicClient ?? workspaceAIClients.getAnthropicClient())
|
||||
|
||||
const anthropicParams = {
|
||||
model: config.model,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import OpenAI from 'openai'
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
import type OpenAI from 'openai'
|
||||
import type Anthropic from '@anthropic-ai/sdk'
|
||||
import type {
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionSystemMessageParam,
|
||||
@@ -8,20 +8,13 @@ import type {
|
||||
import type { AIProviderModel } from '$lib/gen'
|
||||
import { getCompletion, parseOpenAICompletion } from '../lib'
|
||||
import { getAnthropicCompletion, parseAnthropicCompletion } from './anthropic'
|
||||
import {
|
||||
getOpenAIResponsesCompletion,
|
||||
parseOpenAIResponsesCompletion
|
||||
} from './openai-responses'
|
||||
import { getOpenAIResponsesCompletion, parseOpenAIResponsesCompletion } from './openai-responses'
|
||||
import type { Tool, ToolCallbacks } from './shared'
|
||||
import {
|
||||
addChatTokenUsage,
|
||||
emptyChatTokenUsage,
|
||||
type ChatTokenUsage
|
||||
} from './tokenUsage'
|
||||
import { addChatTokenUsage, emptyChatTokenUsage, type ChatTokenUsage } from './tokenUsage'
|
||||
|
||||
export interface ChatClients {
|
||||
openai: OpenAI
|
||||
anthropic: Anthropic
|
||||
openai: Promise<OpenAI>
|
||||
anthropic: Promise<Anthropic>
|
||||
}
|
||||
|
||||
export interface ChatLoopConfig {
|
||||
@@ -137,10 +130,7 @@ export async function runChatLoop(config: ChatLoopConfig): Promise<ChatLoopResul
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'OpenAI Responses API failed, falling back to Completions API:',
|
||||
err
|
||||
)
|
||||
console.warn('OpenAI Responses API failed, falling back to Completions API:', err)
|
||||
const errorMessage = err instanceof Error ? err.message : String(err)
|
||||
if (errorMessage.includes('Responses API is not enabled')) {
|
||||
skipResponsesApi = true
|
||||
@@ -172,15 +162,10 @@ export async function runChatLoop(config: ChatLoopConfig): Promise<ChatLoopResul
|
||||
}
|
||||
}
|
||||
} else if (isAnthropic) {
|
||||
const completion = await getAnthropicCompletion(
|
||||
messageParams,
|
||||
abortController,
|
||||
toolDefs,
|
||||
{
|
||||
forceModelProvider: modelProvider,
|
||||
anthropicClient: clients.anthropic
|
||||
}
|
||||
)
|
||||
const completion = await getAnthropicCompletion(messageParams, abortController, toolDefs, {
|
||||
forceModelProvider: modelProvider,
|
||||
anthropicClient: clients.anthropic
|
||||
})
|
||||
if (completion) {
|
||||
const continueCompletion = await parseAnthropicCompletion(
|
||||
completion,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import OpenAI, { OpenAIError } from 'openai'
|
||||
import type OpenAI from 'openai'
|
||||
import type { OpenAIError } from 'openai'
|
||||
import type {
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionMessageFunctionToolCall,
|
||||
@@ -14,10 +15,7 @@ import {
|
||||
import { processToolCall, type Tool, type ToolCallbacks } from './shared'
|
||||
import type { ResponseStream } from 'openai/lib/responses/ResponseStream.mjs'
|
||||
import type { AIProviderModel } from '$lib/gen'
|
||||
import {
|
||||
openAIResponsesUsageToChatTokenUsage,
|
||||
type ChatTokenUsage
|
||||
} from './tokenUsage'
|
||||
import { openAIResponsesUsageToChatTokenUsage, type ChatTokenUsage } from './tokenUsage'
|
||||
|
||||
interface ParsedCompletionResult {
|
||||
shouldContinue: boolean
|
||||
@@ -137,7 +135,7 @@ export async function getOpenAIResponsesCompletion(
|
||||
tools?: OpenAI.Chat.Completions.ChatCompletionTool[],
|
||||
options?: {
|
||||
forceModelProvider?: AIProviderModel
|
||||
openaiClient?: OpenAI
|
||||
openaiClient?: OpenAI | Promise<OpenAI>
|
||||
}
|
||||
) {
|
||||
const { provider, config } = getProviderAndCompletionConfig({
|
||||
@@ -149,7 +147,7 @@ export async function getOpenAIResponsesCompletion(
|
||||
const { instructions, input } = convertMessagesToResponsesInput(messages)
|
||||
const responsesConfig = convertCompletionConfigToResponsesConfig(config)
|
||||
|
||||
const client = options?.openaiClient ?? workspaceAIClients.getOpenaiClient()
|
||||
const client = await (options?.openaiClient ?? workspaceAIClients.getOpenaiClient())
|
||||
|
||||
const runner = client.responses.stream(
|
||||
{
|
||||
@@ -168,17 +166,20 @@ export async function getOpenAIResponsesCompletion(
|
||||
return runner
|
||||
}
|
||||
|
||||
// Wrapper that converts ResponseStream to ChatCompletionChunk format for lib.ts usage
|
||||
export async function* getOpenAIResponsesCompletionStream(
|
||||
// Wrapper that converts ResponseStream to ChatCompletionChunk format for lib.ts usage.
|
||||
// Returns an AsyncIterable so setup failures (SDK dynamic import, client construction,
|
||||
// responses.stream() request) surface at the await in the caller and the caller can
|
||||
// fall back to the Completions API.
|
||||
export async function getOpenAIResponsesCompletionStream(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
abortController: AbortController,
|
||||
tools?: OpenAI.Chat.Completions.ChatCompletionTool[]
|
||||
): AsyncGenerator<OpenAI.Chat.Completions.ChatCompletionChunk> {
|
||||
): Promise<AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>> {
|
||||
const { provider, config } = getProviderAndCompletionConfig({ messages, stream: true, tools })
|
||||
const { instructions, input } = convertMessagesToResponsesInput(messages)
|
||||
const responsesConfig = convertCompletionConfigToResponsesConfig(config)
|
||||
|
||||
const openaiClient = workspaceAIClients.getOpenaiClient()
|
||||
const openaiClient = await workspaceAIClients.getOpenaiClient()
|
||||
|
||||
const runner = openaiClient.responses.stream(
|
||||
{
|
||||
@@ -194,27 +195,31 @@ export async function* getOpenAIResponsesCompletionStream(
|
||||
}
|
||||
)
|
||||
|
||||
// Convert ResponseStream events to ChatCompletionChunk format
|
||||
for await (const event of runner) {
|
||||
if (event.type === 'response.output_text.delta') {
|
||||
// Yield text chunks in ChatCompletionChunk format
|
||||
yield {
|
||||
id: 'chatcmpl-' + Date.now(),
|
||||
object: 'chat.completion.chunk',
|
||||
created: Date.now(),
|
||||
model: responsesConfig.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
content: event.delta || ''
|
||||
},
|
||||
finish_reason: null
|
||||
}
|
||||
]
|
||||
} as OpenAI.Chat.Completions.ChatCompletionChunk
|
||||
async function* iterate() {
|
||||
// Convert ResponseStream events to ChatCompletionChunk format
|
||||
for await (const event of runner) {
|
||||
if (event.type === 'response.output_text.delta') {
|
||||
// Yield text chunks in ChatCompletionChunk format
|
||||
yield {
|
||||
id: 'chatcmpl-' + Date.now(),
|
||||
object: 'chat.completion.chunk',
|
||||
created: Date.now(),
|
||||
model: responsesConfig.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
content: event.delta || ''
|
||||
},
|
||||
finish_reason: null
|
||||
}
|
||||
]
|
||||
} as OpenAI.Chat.Completions.ChatCompletionChunk
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return iterate()
|
||||
}
|
||||
|
||||
export async function parseOpenAIResponsesCompletion(
|
||||
@@ -417,11 +422,11 @@ export async function getNonStreamingOpenAIResponsesCompletion(
|
||||
}
|
||||
}
|
||||
|
||||
const openaiClient = testOptions?.apiKey
|
||||
const openaiClient = await (testOptions?.apiKey
|
||||
? createOpenAIProxyClient(getAiProxyBaseURL())
|
||||
: testOptions?.workspace
|
||||
? workspaceAIClients.createOpenaiClient(testOptions.workspace)
|
||||
: workspaceAIClients.getOpenaiClient()
|
||||
: workspaceAIClients.getOpenaiClient())
|
||||
|
||||
const response = await openaiClient.responses.create(
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AIProvider, AIProviderModel } from '$lib/gen'
|
||||
import { workspaceStore, type DBSchema, type SQLSchema } from '$lib/stores'
|
||||
import type { IntrospectionQuery } from 'graphql'
|
||||
import OpenAI from 'openai'
|
||||
import type OpenAI from 'openai'
|
||||
import type {
|
||||
ChatCompletionChunk,
|
||||
ChatCompletionCreateParams,
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
ChatCompletionMessageFunctionToolCall,
|
||||
ChatCompletionMessageParam
|
||||
} from 'openai/resources/index.mjs'
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
import type Anthropic from '@anthropic-ai/sdk'
|
||||
import { get, type Writable } from 'svelte/store'
|
||||
import { OpenAPI, ResourceService, type Script } from '../../gen'
|
||||
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
|
||||
@@ -389,8 +389,9 @@ export function getAiProxyBaseURL(workspace?: string): string {
|
||||
: `${location.origin}${OpenAPI.BASE}/ai/proxy`
|
||||
}
|
||||
|
||||
export function createOpenAIProxyClient(baseURL: string): OpenAI {
|
||||
return new OpenAI({
|
||||
export async function createOpenAIProxyClient(baseURL: string): Promise<OpenAI> {
|
||||
const { default: OpenAIClass } = await import('openai')
|
||||
return new OpenAIClass({
|
||||
baseURL,
|
||||
apiKey: 'fake-key',
|
||||
defaultHeaders: {
|
||||
@@ -400,8 +401,9 @@ export function createOpenAIProxyClient(baseURL: string): OpenAI {
|
||||
})
|
||||
}
|
||||
|
||||
export function createAnthropicProxyClient(baseURL: string): Anthropic {
|
||||
return new Anthropic({
|
||||
export async function createAnthropicProxyClient(baseURL: string): Promise<Anthropic> {
|
||||
const { default: AnthropicClass } = await import('@anthropic-ai/sdk')
|
||||
return new AnthropicClass({
|
||||
baseURL,
|
||||
apiKey: 'fake-key',
|
||||
dangerouslyAllowBrowser: true,
|
||||
@@ -410,34 +412,38 @@ export function createAnthropicProxyClient(baseURL: string): Anthropic {
|
||||
}
|
||||
|
||||
class WorkspacedAIClients {
|
||||
private openaiClient: OpenAI | undefined
|
||||
private anthropicClient: Anthropic | undefined
|
||||
private baseURL: string | undefined
|
||||
private openaiClient: Promise<OpenAI> | undefined
|
||||
private anthropicClient: Promise<Anthropic> | undefined
|
||||
|
||||
init(workspace: string) {
|
||||
this.openaiClient = this.createOpenaiClient(workspace)
|
||||
this.anthropicClient = this.createAnthropicClient(workspace)
|
||||
this.baseURL = getAiProxyBaseURL(workspace)
|
||||
// Invalidate cached clients; they will be constructed on first use,
|
||||
// so the SDK chunks only load when Copilot is actually invoked.
|
||||
this.openaiClient = undefined
|
||||
this.anthropicClient = undefined
|
||||
}
|
||||
|
||||
createOpenaiClient(workspace: string): OpenAI {
|
||||
createOpenaiClient(workspace: string): Promise<OpenAI> {
|
||||
return createOpenAIProxyClient(getAiProxyBaseURL(workspace))
|
||||
}
|
||||
|
||||
createAnthropicClient(workspace: string): Anthropic {
|
||||
createAnthropicClient(workspace: string): Promise<Anthropic> {
|
||||
return createAnthropicProxyClient(getAiProxyBaseURL(workspace))
|
||||
}
|
||||
|
||||
getOpenaiClient() {
|
||||
if (!this.openaiClient) {
|
||||
getOpenaiClient(): Promise<OpenAI> {
|
||||
if (!this.baseURL) {
|
||||
throw new Error('OpenAI not initialized')
|
||||
}
|
||||
return this.openaiClient
|
||||
return (this.openaiClient ??= createOpenAIProxyClient(this.baseURL))
|
||||
}
|
||||
|
||||
getAnthropicClient() {
|
||||
if (!this.anthropicClient) {
|
||||
getAnthropicClient(): Promise<Anthropic> {
|
||||
if (!this.baseURL) {
|
||||
throw new Error('Anthropic not initialized')
|
||||
}
|
||||
return this.anthropicClient
|
||||
return (this.anthropicClient ??= createAnthropicProxyClient(this.baseURL))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,11 +528,11 @@ async function testAnthropicKey({
|
||||
headers['X-API-Key'] = apiKey
|
||||
}
|
||||
|
||||
const anthropicClient = apiKey
|
||||
const anthropicClient = await (apiKey
|
||||
? createAnthropicProxyClient(getAiProxyBaseURL())
|
||||
: workspace
|
||||
? workspaceAIClients.createAnthropicClient(workspace)
|
||||
: workspaceAIClients.getAnthropicClient()
|
||||
: workspaceAIClients.getAnthropicClient())
|
||||
|
||||
await anthropicClient.messages.create(
|
||||
{
|
||||
@@ -800,11 +806,11 @@ export async function getNonStreamingCompletion(
|
||||
'X-API-Key': testOptions.apiKey
|
||||
}
|
||||
}
|
||||
const openaiClient = testOptions?.apiKey
|
||||
const openaiClient = await (testOptions?.apiKey
|
||||
? createOpenAIProxyClient(getAiProxyBaseURL())
|
||||
: testOptions?.workspace
|
||||
? workspaceAIClients.createOpenaiClient(testOptions.workspace)
|
||||
: workspaceAIClients.getOpenaiClient()
|
||||
: workspaceAIClients.getOpenaiClient())
|
||||
|
||||
const completion = await openaiClient.chat.completions.create(config, fetchOptions)
|
||||
response = completion.choices?.[0]?.message.content || ''
|
||||
@@ -890,7 +896,7 @@ export async function getCompletion(
|
||||
options?: {
|
||||
forceCompletions?: boolean
|
||||
forceModelProvider?: AIProviderModel
|
||||
openaiClient?: OpenAI
|
||||
openaiClient?: OpenAI | Promise<OpenAI>
|
||||
}
|
||||
): Promise<Stream<ChatCompletionChunk>> {
|
||||
const { provider, config } = getProviderAndCompletionConfig({
|
||||
@@ -900,10 +906,16 @@ export async function getCompletion(
|
||||
forceModelProvider: options?.forceModelProvider
|
||||
})
|
||||
|
||||
// Use Responses API for OpenAI and Azure OpenAI
|
||||
// Use Responses API for OpenAI and Azure OpenAI. Setup failures (dynamic
|
||||
// SDK import, client construction, initial request) now surface here and
|
||||
// fall through to the Completions API below.
|
||||
if ((provider === 'openai' || provider === 'azure_openai') && !options?.forceCompletions) {
|
||||
try {
|
||||
const stream = getOpenAIResponsesCompletionStream(messages, abortController, tools) as any
|
||||
const stream = (await getOpenAIResponsesCompletionStream(
|
||||
messages,
|
||||
abortController,
|
||||
tools
|
||||
)) as any
|
||||
return stream
|
||||
} catch (error) {
|
||||
console.error('Error using Responses API:', error)
|
||||
@@ -911,7 +923,7 @@ export async function getCompletion(
|
||||
}
|
||||
|
||||
// Use Completions API for other providers
|
||||
const client = options?.openaiClient ?? workspaceAIClients.getOpenaiClient()
|
||||
const client = await (options?.openaiClient ?? workspaceAIClients.getOpenaiClient())
|
||||
const completionConfig =
|
||||
(provider === 'openai' || provider === 'azure_openai' || provider === 'googleai') &&
|
||||
config.stream
|
||||
|
||||
Reference in New Issue
Block a user