feat(flows): allow all providers for ai agent steps (#6529)

* add ui for structured output

* implement backend logic for openai models

* simulate having non required props

* cleaning

* implement logic for anthropic

* cleaning

* cleaning

* cleaning

* avoid name clash

* return object

* focus new field when adding

* fix condition

* small opti

* use box raw value

* avoid unnecessary parsing + return error if parsing fails

* update ui to show all providers

* cleaning

* adapt backend

* fix

* styling fix

* fix for anthropic with openrouter

* nit styling

* avoid race condition

* fix merge

* fix

* fix import

* make one component for whole provider selection

* fix resource logic

* rename

* clean

* remove console log

* fix bad copy
This commit is contained in:
centdix
2025-09-05 08:11:23 +00:00
committed by GitHub
parent 548c4da147
commit ac62b4da1c
13 changed files with 414 additions and 186 deletions
+1 -84
View File
@@ -10,6 +10,7 @@ use reqwest::{Client, RequestBuilder};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use std::collections::HashMap;
use windmill_common::ai_providers::{AIProvider, ProviderConfig, ProviderModel};
use windmill_audit::{audit_oss::audit_log, ActionKind};
use windmill_common::error::{to_anyhow, Error, Result};
@@ -261,90 +262,6 @@ impl ExpiringAIRequestConfig {
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Clone)]
#[serde(rename_all = "lowercase")]
pub enum AIProvider {
OpenAI,
#[serde(rename = "azure_openai")]
AzureOpenAI,
Anthropic,
Mistral,
DeepSeek,
GoogleAI,
Groq,
OpenRouter,
TogetherAI,
CustomAI,
}
impl AIProvider {
pub async fn get_base_url(&self, resource_base_url: Option<String>, db: &DB) -> Result<String> {
match self {
AIProvider::OpenAI => {
let azure_base_path = sqlx::query_scalar!(
"SELECT value
FROM global_settings
WHERE name = 'openai_azure_base_path'",
)
.fetch_optional(db)
.await?;
let azure_base_path = if let Some(azure_base_path) = azure_base_path {
Some(
serde_json::from_value::<String>(azure_base_path).map_err(|e| {
Error::internal_err(format!("validating openai azure base path {e:#}"))
})?,
)
} else {
OPENAI_AZURE_BASE_PATH.clone()
};
Ok(azure_base_path.unwrap_or(OPENAI_BASE_URL.to_string()))
}
AIProvider::DeepSeek => Ok("https://api.deepseek.com/v1".to_string()),
AIProvider::GoogleAI => {
Ok("https://generativelanguage.googleapis.com/v1beta/openai".to_string())
}
AIProvider::Groq => Ok("https://api.groq.com/openai/v1".to_string()),
AIProvider::OpenRouter => Ok("https://openrouter.ai/api/v1".to_string()),
AIProvider::TogetherAI => Ok("https://api.together.xyz/v1".to_string()),
AIProvider::Anthropic => Ok("https://api.anthropic.com/v1".to_string()),
AIProvider::Mistral => Ok("https://api.mistral.ai/v1".to_string()),
p @ (AIProvider::CustomAI | AIProvider::AzureOpenAI) => {
if let Some(base_url) = resource_base_url {
Ok(base_url)
} else {
Err(Error::BadRequest(format!(
"{:?} provider requires a base URL in the resource",
p
)))
}
}
}
}
}
impl TryFrom<&str> for AIProvider {
type Error = Error;
fn try_from(s: &str) -> Result<Self> {
let s = serde_json::from_value::<AIProvider>(serde_json::Value::String(s.to_string()))
.map_err(|e| Error::BadRequest(format!("Invalid AI provider: {}", e)))?;
Ok(s)
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ProviderConfig {
pub resource_path: String,
pub models: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ProviderModel {
pub model: String,
pub provider: AIProvider,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AIConfig {
#[serde(skip_serializing_if = "Option::is_none")]
+102
View File
@@ -0,0 +1,102 @@
/*
* This file contains shared AI provider utilities used by both the API and worker.
*/
use crate::db::DB;
use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
lazy_static::lazy_static! {
static ref OPENAI_AZURE_BASE_PATH: Option<String> = std::env::var("OPENAI_AZURE_BASE_PATH").ok();
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Clone)]
#[serde(rename_all = "lowercase")]
pub enum AIProvider {
OpenAI,
#[serde(rename = "azure_openai")]
AzureOpenAI,
Anthropic,
Mistral,
DeepSeek,
GoogleAI,
Groq,
OpenRouter,
TogetherAI,
CustomAI,
}
impl AIProvider {
/// Get the base URL for the AI provider
pub async fn get_base_url(&self, resource_base_url: Option<String>, db: &DB) -> Result<String> {
match self {
AIProvider::OpenAI => {
// Check for Azure base path override
let azure_base_path = sqlx::query_scalar!(
"SELECT value
FROM global_settings
WHERE name = 'openai_azure_base_path'",
)
.fetch_optional(db)
.await?;
let azure_base_path = if let Some(azure_base_path) = azure_base_path {
Some(
serde_json::from_value::<String>(azure_base_path).map_err(|e| {
Error::internal_err(format!("validating openai azure base path {e:#}"))
})?,
)
} else {
OPENAI_AZURE_BASE_PATH.clone()
};
Ok(azure_base_path.unwrap_or("https://api.openai.com/v1".to_string()))
}
AIProvider::DeepSeek => Ok("https://api.deepseek.com/v1".to_string()),
AIProvider::GoogleAI => {
Ok("https://generativelanguage.googleapis.com/v1beta/openai".to_string())
}
AIProvider::Groq => Ok("https://api.groq.com/openai/v1".to_string()),
AIProvider::OpenRouter => Ok("https://openrouter.ai/api/v1".to_string()),
AIProvider::TogetherAI => Ok("https://api.together.xyz/v1".to_string()),
AIProvider::Anthropic => Ok("https://api.anthropic.com/v1".to_string()),
AIProvider::Mistral => Ok("https://api.mistral.ai/v1".to_string()),
p @ (AIProvider::CustomAI | AIProvider::AzureOpenAI) => {
if let Some(base_url) = resource_base_url {
Ok(base_url)
} else {
Err(Error::BadRequest(format!(
"{:?} provider requires a base URL in the resource",
p
)))
}
}
}
}
/// Check if this provider is Anthropic (needs special handling)
pub fn is_anthropic(&self) -> bool {
matches!(self, AIProvider::Anthropic)
}
}
impl TryFrom<&str> for AIProvider {
type Error = Error;
fn try_from(s: &str) -> Result<Self> {
let s = serde_json::from_value::<AIProvider>(serde_json::Value::String(s.to_string()))
.map_err(|e| Error::BadRequest(format!("Invalid AI provider: {}", e)))?;
Ok(s)
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ProviderConfig {
pub resource_path: String,
pub models: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ProviderModel {
pub model: String,
pub provider: AIProvider,
}
+1
View File
@@ -26,6 +26,7 @@ use scripts::ScriptLang;
use sqlx::{Acquire, Postgres};
pub mod agent_workers;
pub mod ai_providers;
pub mod apps;
pub mod assets;
pub mod auth;
+20 -21
View File
@@ -6,6 +6,7 @@ use std::{collections::HashMap, sync::Arc};
#[cfg(feature = "benchmark")]
use windmill_common::bench::BenchmarkIter;
use windmill_common::{
ai_providers::AIProvider,
auth::get_job_perms,
cache,
client::AuthedClient,
@@ -155,7 +156,7 @@ struct Tool {
#[derive(Deserialize, Debug)]
struct AIAgentArgs {
provider: Provider,
provider: ProviderWithResource,
system_prompt: Option<String>,
user_message: String,
temperature: Option<f32>,
@@ -167,35 +168,30 @@ struct AIAgentArgs {
struct ProviderResource {
#[serde(alias = "apiKey")]
api_key: String,
#[serde(alias = "baseUrl")]
base_url: Option<String>,
}
#[derive(Deserialize, Debug)]
#[serde(tag = "kind")]
enum Provider {
OpenAI { resource: ProviderResource, model: String },
Anthropic { resource: ProviderResource, model: String },
struct ProviderWithResource {
kind: AIProvider,
resource: ProviderResource,
model: String,
}
impl Provider {
impl ProviderWithResource {
fn get_api_key(&self) -> &str {
match self {
Provider::OpenAI { resource, .. } => &resource.api_key,
Provider::Anthropic { resource, .. } => &resource.api_key,
}
&self.resource.api_key
}
fn get_model(&self) -> &str {
match self {
Provider::OpenAI { model, .. } => model,
Provider::Anthropic { model, .. } => model,
}
&self.model
}
fn get_base_url(&self) -> &str {
match self {
Provider::OpenAI { .. } => "https://api.openai.com/v1",
Provider::Anthropic { .. } => "https://api.anthropic.com/v1",
}
async fn get_base_url(&self, db: &DB) -> Result<String, Error> {
self.kind
.get_base_url(self.resource.base_url.clone(), db)
.await
}
}
@@ -765,7 +761,7 @@ async fn run_agent(
let mut content = None;
let base_url = args.provider.get_base_url();
let base_url = args.provider.get_base_url(db).await?;
let api_key = args.provider.get_api_key();
let mut tool_defs: Option<Vec<ToolDef>> = if tools.is_empty() {
@@ -780,7 +776,10 @@ async fn run_agent(
.and_then(|schema| schema.properties.as_ref())
.map(|props| !props.is_empty())
.unwrap_or(false);
let is_anthropic = matches!(args.provider, Provider::Anthropic { .. });
let provider_is_anthropic = args.provider.kind.is_anthropic();
let is_openrouter_anthropic = args.provider.kind == AIProvider::OpenRouter
&& args.provider.model.starts_with("anthropic/");
let is_anthropic = provider_is_anthropic || is_openrouter_anthropic;
let mut response_format: Option<ResponseFormat> = None;
let mut used_structured_output_tool = false;
let mut structured_output_tool_name: Option<String> = None;
@@ -0,0 +1,197 @@
<script lang="ts">
import { type Snippet } from 'svelte'
import Select from './select/Select.svelte'
import { fetchAvailableModels, AI_PROVIDERS } from './copilot/lib'
import type { AIProvider } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { get } from 'svelte/store'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ResourcePicker from './ResourcePicker.svelte'
interface ProviderValue {
kind?: AIProvider
resource?: string
model?: string
}
interface Props {
value: ProviderValue | undefined
disabled?: boolean
actions?: Snippet
}
let { value = $bindable(), disabled = false, actions }: Props = $props()
// Initialize value if undefined
if (!value) {
value = {}
}
let loading = $state(false)
let availableModels = $state<string[]>([])
let filterText = $state('')
let modelsCache = new Map<AIProvider, string[]>()
// Reactive items for the Select component
let items = $derived(
availableModels.map((model) => ({
label: model,
value: model
}))
)
// Provider options for the toggle button group
const providerOptions = Object.entries(AI_PROVIDERS).map(([key, details]) => ({
value: key as AIProvider,
label: details.label
}))
async function loadModels(signal?: AbortSignal) {
const provider = value?.kind
const resourceValue = value?.resource
const resourcePath = resourceValueToPath(resourceValue)
if (!provider || !resourcePath) {
return
}
loading = true
if (modelsCache.has(provider)) {
availableModels = modelsCache.get(provider) || []
loading = false
return
}
try {
const workspace = get(workspaceStore) || ''
const models = await fetchAvailableModels(resourcePath, workspace, provider, signal)
if (signal?.aborted) {
return
}
availableModels = models
modelsCache.set(provider, models)
} catch (e) {
if (signal?.aborted) {
return
}
// Fall back to default models for this provider
const defaultModels = AI_PROVIDERS[provider]?.defaultModels || []
availableModels = defaultModels
} finally {
if (!signal?.aborted) {
loading = false
}
}
}
// Reload models when provider or resourcePath changes
$effect(() => {
const abortController = new AbortController()
const provider = value?.kind
const resourceValue = value?.resource
const resourcePath = resourceValueToPath(resourceValue)
filterText = ''
if (value) {
value.model = undefined
}
if (provider && resourcePath) {
loadModels(abortController.signal)
} else {
const defaultModels = provider ? AI_PROVIDERS[provider]?.defaultModels || [] : []
availableModels = defaultModels
loading = false
}
return () => {
abortController.abort()
}
})
// Handle provider selection
function onProviderChange(selectedProvider: AIProvider) {
if (value) {
value.kind = selectedProvider
value.resource = undefined
value.model = undefined
}
}
// Helper functions to handle $res: prefix like ObjectResourceInput does
function isResource(resourceValue: any): boolean {
return (
typeof resourceValue === 'string' &&
resourceValue.length >= '$res:'.length &&
resourceValue.startsWith('$res:')
)
}
function resourceValueToPath(resourceValue: any): string | undefined {
if (isResource(resourceValue)) {
return resourceValue.substring('$res:'.length)
}
return resourceValue
}
function pathToResourceValue(path: string | undefined): string | undefined {
if (path == undefined) {
return undefined
} else {
return `$res:${path}`
}
}
</script>
<div class="w-full flex flex-col gap-3">
<!-- Provider Selection -->
<div class="flex flex-col gap-2">
<ToggleButtonGroup selected={value?.kind} onSelected={onProviderChange} {disabled}>
{#snippet children({ item })}
{#each providerOptions as option}
<ToggleButton value={option.value} label={option.label} {item} />
{/each}
{/snippet}
</ToggleButtonGroup>
</div>
<!-- Resource Selection -->
<div class="flex flex-col border border-gray-200 rounded-md p-2 gap-2">
<div class="flex flex-col gap-1">
<p class="text-sm font-medium text-primary">resource</p>
<ResourcePicker
bind:value={
() => resourceValueToPath(value?.resource),
(v) => {
if (value) {
value.resource = pathToResourceValue(v)
}
}
}
resourceType={value?.kind}
disabled={disabled || !value?.kind}
placeholder="Select resource"
selectFirst={true}
/>
</div>
<!-- Model Selection -->
<div class="flex flex-col gap-1">
<p class="text-sm font-medium text-primary">model</p>
<Select
{items}
bind:value={value.model}
placeholder="Select model"
disabled={disabled || !value?.kind || !resourceValueToPath(value?.resource)}
{loading}
clearable={false}
noItemsMsg={'No models available'}
bind:filterText
/>
</div>
</div>
{@render actions?.()}
</div>
+4 -1
View File
@@ -45,6 +45,7 @@
import { base } from '$lib/base'
import { workspaceStore } from '$lib/stores'
import { getJsonSchemaFromResource } from './schema/jsonSchemaResource.svelte'
import AIProviderPicker from './AIProviderPicker.svelte'
interface Props {
label?: string
@@ -683,7 +684,7 @@
delete value.properties[e.detail]
// Also remove from order array if it exists
if (value.order) {
value.order = value.order.filter(key => key !== e.detail)
value.order = value.order.filter((key) => key !== e.detail)
}
// Update the value to trigger reactivity
value = { ...value }
@@ -1268,6 +1269,8 @@
: undefined}
{showSchemaExplorer}
/>
{:else if inputCat == 'ai-provider'}
<AIProviderPicker bind:value {disabled} {actions} />
{:else if inputCat == 'email'}
<input
{autofocus}
@@ -61,14 +61,19 @@
<div
use:melt={$root}
class={twMerge(
`h-8 flex ${noWFull ? '' : 'w-full'} ${disabled ? 'disabled' : ''}`,
`flex ${noWFull ? '' : 'w-full'} ${disabled ? 'disabled' : ''}`,
className,
'flex items-center data-[orientation="vertical"]:flex-col'
)}
aria-label="Toggle button group"
{id}
>
<div class={twMerge('flex bg-surface-secondary rounded-md p-0.5 gap-1 h-full ', tabListClass)}>
<div
class={twMerge(
'flex bg-surface-secondary rounded-md p-0.5 gap-1 h-full flex-wrap',
tabListClass
)}
>
{@render children?.({ item, disabled })}
</div>
</div>
@@ -324,7 +324,7 @@ Generate a tool name for the script below:
on:blur={() => (focused = false)}
/>
{#if promptConfigName === 'agentToolFunctionName' && !validateToolName(content ?? '')}
<div class="text-3xs text-red-400 absolute -bottom-4">
<div class="text-3xs text-red-400 -mt-0.5">
Invalid tool name, should only contain letters, numbers and underscores
</div>
{/if}
+64 -16
View File
@@ -27,6 +27,11 @@ import type { Stream } from 'openai/core/streaming.mjs'
export const SUPPORTED_LANGUAGES = new Set(Object.keys(GEN_CONFIG.prompts))
interface AIProviderDetails {
label: string
defaultModels: string[]
}
const OPENAI_MODELS = [
'gpt-5',
'gpt-5-mini',
@@ -38,18 +43,47 @@ const OPENAI_MODELS = [
'o3-mini'
]
// need at least one model for each provider except customai
export const AI_DEFAULT_MODELS: Record<AIProvider, string[]> = {
openai: OPENAI_MODELS,
azure_openai: OPENAI_MODELS,
anthropic: ['claude-sonnet-4-0', 'claude-sonnet-4-0/thinking', 'claude-3-5-haiku-latest'],
mistral: ['codestral-latest'],
deepseek: ['deepseek-chat', 'deepseek-reasoner'],
googleai: ['gemini-2.0-flash', 'gemini-1.5-flash', 'gemini-1.5-pro'],
groq: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant'],
openrouter: ['meta-llama/llama-3.2-3b-instruct:free'],
togetherai: ['meta-llama/Llama-3.3-70B-Instruct-Turbo'],
customai: []
export const AI_PROVIDERS: Record<AIProvider, AIProviderDetails> = {
openai: {
label: 'OpenAI',
defaultModels: OPENAI_MODELS
},
azure_openai: {
label: 'Azure OpenAI',
defaultModels: OPENAI_MODELS
},
anthropic: {
label: 'Anthropic',
defaultModels: ['claude-sonnet-4-0', 'claude-sonnet-4-0/thinking', 'claude-3-5-haiku-latest']
},
mistral: {
label: 'Mistral',
defaultModels: ['codestral-latest']
},
deepseek: {
label: 'DeepSeek',
defaultModels: ['deepseek-chat', 'deepseek-reasoner']
},
googleai: {
label: 'Google AI',
defaultModels: ['gemini-2.0-flash', 'gemini-1.5-flash', 'gemini-1.5-pro']
},
groq: {
label: 'Groq',
defaultModels: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant']
},
openrouter: {
label: 'OpenRouter',
defaultModels: ['meta-llama/llama-3.2-3b-instruct:free']
},
togetherai: {
label: 'Together AI',
defaultModels: ['meta-llama/Llama-3.3-70B-Instruct-Turbo']
},
customai: {
label: 'Custom AI',
defaultModels: []
}
}
export interface ModelResponse {
@@ -67,9 +101,11 @@ export interface ModelResponse {
export async function fetchAvailableModels(
resourcePath: string,
workspace: string,
provider: AIProvider
provider: AIProvider,
signal?: AbortSignal
): Promise<string[]> {
const models = await fetch(`${location.origin}${OpenAPI.BASE}/w/${workspace}/ai/proxy/models`, {
signal,
headers: {
'X-Resource-Path': resourcePath,
'X-Provider': provider,
@@ -82,6 +118,16 @@ export async function fetchAvailableModels(
}
const data = (await models.json()) as { data: ModelResponse[] }
if (data.data.length > 0) {
const sortFunc = (provider: AIProvider) => (a: string, b: string) => {
// First prioritize models in defaultModels array
const defaultModels = AI_PROVIDERS[provider]?.defaultModels || []
const aInDefault = defaultModels.includes(a)
const bInDefault = defaultModels.includes(b)
if (aInDefault && !bInDefault) return -1
if (!aInDefault && bInDefault) return 1
return 0
}
switch (provider) {
case 'openai':
return data.data
@@ -89,6 +135,7 @@ export async function fetchAvailableModels(
(m) => m.id.startsWith('gpt-') || m.id.startsWith('o') || m.id.startsWith('codex')
)
.map((m) => m.id)
.sort(sortFunc(provider))
case 'azure_openai':
return data.data
.filter(
@@ -98,10 +145,11 @@ export async function fetchAvailableModels(
(m.capabilities.completion || m.capabilities.chat_completion)
)
.map((m) => m.id)
.sort(sortFunc(provider))
case 'googleai':
return data.data.map((m) => m.id.split('/')[1])
return data.data.map((m) => m.id.split('/')[1]).sort(sortFunc(provider))
default:
return data.data.map((m) => m.id)
return data.data.map((m) => m.id).sort(sortFunc(provider))
}
}
@@ -291,7 +339,7 @@ export async function testKey({
if (!apiKey && !resourcePath) {
throw new Error('API key or resource path is required')
}
const modelToTest = model ?? AI_DEFAULT_MODELS[aiProvider][0]
const modelToTest = model ?? AI_PROVIDERS[aiProvider].defaultModels[0]
if (!modelToTest) {
throw new Error('Missing a model to test')
@@ -60,41 +60,7 @@ export async function loadSchemaFromModule(module: FlowModule): Promise<{
properties: {
provider: {
type: 'object',
oneOf: [
{
type: 'object',
title: 'OpenAI',
properties: {
kind: { type: 'string', enum: ['OpenAI'] },
resource: {
type: 'object',
format: 'resource-openai'
},
model: {
type: 'string',
enum: ['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-4.1', 'gpt-4o', 'gpt-4o-mini']
}
},
required: ['kind', 'resource', 'model']
},
{
type: 'object',
title: 'Anthropic',
properties: {
kind: { type: 'string', enum: ['Anthropic'] },
resource: {
type: 'object',
format: 'resource-anthropic'
},
model: {
type: 'string',
enum: ['claude-sonnet-4-0', 'claude-3-7-sonnet-latest', 'claude-3-5-haiku-latest']
}
},
required: ['kind', 'resource', 'model']
}
]
format: 'ai-provider'
},
user_message: {
type: 'string'
@@ -2,7 +2,7 @@
import { WorkspaceService, type AIConfig, type AIProvider } from '$lib/gen'
import { setCopilotInfo, workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { AI_DEFAULT_MODELS, fetchAvailableModels } from '../copilot/lib'
import { AI_PROVIDERS, fetchAvailableModels } from '../copilot/lib'
import TestAiKey from '../copilot/TestAIKey.svelte'
import Description from '../Description.svelte'
import Label from '../Label.svelte'
@@ -19,19 +19,6 @@
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
import autosize from '$lib/autosize'
const aiProviderLabels: [AIProvider, string][] = [
['openai', 'OpenAI'],
['azure_openai', 'Azure OpenAI'],
['anthropic', 'Anthropic'],
['mistral', 'Mistral'],
['deepseek', 'DeepSeek'],
['googleai', 'Google AI'],
['groq', 'Groq'],
['openrouter', 'OpenRouter'],
['togetherai', 'Together AI'],
['customai', 'Custom AI']
]
const MAX_CUSTOM_PROMPT_LENGTH = 5000
let {
@@ -51,7 +38,7 @@
let fetchedAiModels = $state(false)
let availableAiModels = $state(
Object.fromEntries(
aiProviderLabels.map(([provider]) => [provider, AI_DEFAULT_MODELS[provider]])
Object.keys(AI_PROVIDERS).map((provider) => [provider, AI_PROVIDERS[provider].defaultModels])
) as Record<AIProvider, string[]>
)
@@ -88,7 +75,7 @@
availableAiModels[provider] = models
} catch (e) {
console.error('failed to fetch models for provider', provider, e)
availableAiModels[provider] = AI_DEFAULT_MODELS[provider]
availableAiModels[provider] = AI_PROVIDERS[provider].defaultModels
}
}
fetchedAiModels = true
@@ -142,7 +129,7 @@
availableAiModels[provider] = models
} catch (e) {
console.error('failed to fetch models for provider', provider, e)
availableAiModels[provider] = AI_DEFAULT_MODELS[provider]
availableAiModels[provider] = AI_PROVIDERS[provider].defaultModels
}
}
@@ -169,12 +156,12 @@
<div class="flex flex-col gap-2">
<p class="font-semibold">AI Providers</p>
<div class="flex flex-col gap-4">
{#each aiProviderLabels as [provider, label]}
{#each Object.entries(AI_PROVIDERS) as [provider, details]}
<div class="flex flex-col gap-2">
<div class="flex flex-row gap-2">
<Toggle
options={{
right: label
right: details.label
}}
checked={!!aiProviders[provider]}
on:change={(e) => {
@@ -240,12 +227,12 @@
() => aiProviders[provider].resource_path,
(v) => {
aiProviders[provider].resource_path = v
onAiProviderChange(provider)
onAiProviderChange(provider as AIProvider)
}
}
/>
<TestAiKey
aiProvider={provider}
aiProvider={provider as AIProvider}
resourcePath={aiProviders[provider].resource_path}
model={aiProviders[provider].models[0]}
/>
+3
View File
@@ -593,6 +593,7 @@ export type InputCat =
| 'oneOf'
| 'dynamic'
| 'json-schema'
| 'ai-provider'
export namespace DynamicInput {
const DYN_FORMAT_PREFIX = ['dynmultiselect-', 'dynselect-']
@@ -657,6 +658,8 @@ export function setInputCat(
return 'list'
} else if (type == 'object' && format?.startsWith('resource')) {
return 'resource-object'
} else if (type == 'object' && format == 'ai-provider') {
return 'ai-provider'
} else if (type == 'object' && DynamicInput.isDynInputFormat(format)) {
return 'dynamic'
} else if (!type || type == 'object' || type == 'array') {
@@ -26,7 +26,7 @@
import { isCloudHosted } from '$lib/cloud'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import { AI_DEFAULT_MODELS } from '$lib/components/copilot/lib'
import { AI_PROVIDERS } from '$lib/components/copilot/lib'
const rd = $page.url.searchParams.get('rd')
@@ -115,15 +115,15 @@
providers: {
[selected]: {
resource_path: path,
models: [AI_DEFAULT_MODELS[selected][0]]
models: [AI_PROVIDERS[selected].defaultModels[0]]
}
},
default_model: {
model: AI_DEFAULT_MODELS[selected][0],
model: AI_PROVIDERS[selected].defaultModels[0],
provider: selected
},
code_completion_model: codeCompletionEnabled
? { model: AI_DEFAULT_MODELS[selected][0], provider: selected }
? { model: AI_PROVIDERS[selected].defaultModels[0], provider: selected }
: undefined
}
: {}
@@ -291,7 +291,7 @@
apiKey={aiKey}
disabled={!aiKey}
aiProvider={selected}
model={AI_DEFAULT_MODELS[selected][0]}
model={AI_PROVIDERS[selected].defaultModels[0]}
/>
</div>
{#if aiKey}