From f96378a7977239f0e80cbb20ae2bc13ce2dea01d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 28 Sep 2026 13:32:08 -0700 Subject: [PATCH 1/4] feat(providers): add Kie provider for Claude, GPT, Grok, and Kimi models --- apps/docs/components/icons.tsx | 45 ++ .../content/docs/workflows/blocks/agent.mdx | 1 + apps/sim/app/(landing)/models/utils.ts | 1 + .../settings/components/byok/byok.tsx | 9 + apps/sim/components/icons.tsx | 45 ++ .../components/usage-consumers.tsx | 2 + apps/sim/lib/api-key/byok.ts | 14 + apps/sim/lib/api/contracts/byok-keys.ts | 1 + apps/sim/lib/memory/agent-turn-session.ts | 7 +- apps/sim/lib/tokenization/constants.ts | 5 + apps/sim/providers/anthropic/core.ts | 5 +- .../providers/conversation-continuation.ts | 6 +- apps/sim/providers/history-adapters.test.ts | 2 +- apps/sim/providers/history-adapters.ts | 30 +- apps/sim/providers/kie/index.ts | 77 ++++ apps/sim/providers/kie/transport.test.ts | 90 ++++ apps/sim/providers/kie/transport.ts | 57 +++ apps/sim/providers/kie/utils.ts | 43 ++ apps/sim/providers/models.ts | 432 ++++++++++++++++++ apps/sim/providers/openai/core.ts | 13 +- apps/sim/providers/registry.ts | 2 + apps/sim/providers/types.ts | 1 + apps/sim/providers/utils.ts | 1 + apps/sim/tools/types.ts | 1 + 24 files changed, 877 insertions(+), 13 deletions(-) create mode 100644 apps/sim/providers/kie/index.ts create mode 100644 apps/sim/providers/kie/transport.test.ts create mode 100644 apps/sim/providers/kie/transport.ts create mode 100644 apps/sim/providers/kie/utils.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index efd80c11da7..263a13678ba 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4319,6 +4319,51 @@ export const ZaiIcon = (props: SVGProps) => ( ) +export function KieIcon(props: SVGProps) { + const id = useId() + const gradientId = `kie_gradient_${id}` + + return ( + + Kie + + + + + + + + + + + + + + + + + ) +} + export const KimiIcon = (props: SVGProps) => ( Kimi diff --git a/apps/docs/content/docs/workflows/blocks/agent.mdx b/apps/docs/content/docs/workflows/blocks/agent.mdx index a07a9aa4658..a9494bd2751 100644 --- a/apps/docs/content/docs/workflows/blocks/agent.mdx +++ b/apps/docs/content/docs/workflows/blocks/agent.mdx @@ -170,6 +170,7 @@ Live tool-call chips stream for **OpenAI, Anthropic, Azure Anthropic, Google, Ve | Groq | Full thinking deltas | `groq/openai/gpt-oss-120b`, `groq/openai/gpt-oss-20b`, `groq/openai/gpt-oss-safeguard-20b`, `groq/qwen/qwen3.8-27b`, `groq/qwen/qwen3.6-27b` | | Meta | Not streamed | `muse-spark-1.3`, `muse-spark-1.1` | | Kimi | Full thinking deltas | `kimi-k3`, `kimi-k2.6` | +| Kie | Summaries only | `kie/claude-opus-5-5`, `kie/claude-opus-5`, `kie/claude-fable-5`, `kie/claude-sonnet-5`, `kie/claude-opus-4-8`, `kie/claude-opus-4-7`, `kie/claude-opus-4-6`, `kie/claude-sonnet-4-6`, `kie/claude-opus-4-5`, `kie/claude-sonnet-4-5`, `kie/claude-haiku-4-5`, `kie/gpt-6-astra`, `kie/gpt-6-sol`, `kie/gpt-6-luna`, `kie/gpt-5-6-sol`, `kie/gpt-5-6-terra`, `kie/gpt-5-6-luna`, `kie/gpt-5-5`, `kie/grok-4-6`, `kie/grok-4-5`, `kie/grok-4-3`, `kie/kimi-k3` | | Z.ai | Full thinking deltas | `glm-5.3`, `glm-5.3-flash`, `glm-5.2`, `glm-5.1`, `glm-5`, `glm-5-turbo`, `glm-4.7`, `glm-4.6`, `glm-4.5`, `glm-4.5-air` | {/* agent-stream-capabilities:end */} diff --git a/apps/sim/app/(landing)/models/utils.ts b/apps/sim/app/(landing)/models/utils.ts index 4d5ecadb7b5..61a0947dd61 100644 --- a/apps/sim/app/(landing)/models/utils.ts +++ b/apps/sim/app/(landing)/models/utils.ts @@ -14,6 +14,7 @@ const PROVIDER_PREFIXES: Record = { baseten: ['baseten/'], 'ollama-cloud': ['ollama-cloud/'], groq: ['groq/'], + kie: ['kie/'], openrouter: ['openrouter/'], vllm: ['vllm/'], } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx index 28bf3a7a232..01b8e73789a 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx @@ -24,6 +24,7 @@ import { HunterIOIcon, IcypeasIcon, JinaAIIcon, + KieIcon, KimiIcon, LeadMagicIcon, LinkupIcon, @@ -166,6 +167,13 @@ const PROVIDERS: (BYOKManagerProvider & { id: BYOKProviderId })[] = [ description: 'LLM calls', placeholder: 'Enter your Ollama API key', }, + { + id: 'kie', + name: 'Kie', + icon: KieIcon, + description: 'LLM calls', + placeholder: 'Enter your Kie API key', + }, { id: 'falai', name: 'Fal.ai', @@ -365,6 +373,7 @@ const PROVIDER_SECTIONS: BYOKProviderSection[] = [ 'together', 'baseten', 'ollama-cloud', + 'kie', 'falai', ], }, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index efd80c11da7..263a13678ba 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -4319,6 +4319,51 @@ export const ZaiIcon = (props: SVGProps) => ( ) +export function KieIcon(props: SVGProps) { + const id = useId() + const gradientId = `kie_gradient_${id}` + + return ( + + Kie + + + + + + + + + + + + + + + + + ) +} + export const KimiIcon = (props: SVGProps) => ( Kimi diff --git a/apps/sim/ee/organization-usage/components/usage-consumers.tsx b/apps/sim/ee/organization-usage/components/usage-consumers.tsx index a2559ac2957..4d2baabe827 100644 --- a/apps/sim/ee/organization-usage/components/usage-consumers.tsx +++ b/apps/sim/ee/organization-usage/components/usage-consumers.tsx @@ -13,6 +13,7 @@ import { FireworksIcon, GeminiIcon, GroqIcon, + KieIcon, KimiIcon, LitellmIcon, MetaIcon, @@ -64,6 +65,7 @@ const PROVIDER_ICONS: Readonly + ) } /** diff --git a/apps/sim/providers/conversation-continuation.ts b/apps/sim/providers/conversation-continuation.ts index 84e5ccb3135..f8e06c1ffaf 100644 --- a/apps/sim/providers/conversation-continuation.ts +++ b/apps/sim/providers/conversation-continuation.ts @@ -13,7 +13,7 @@ import { retainCompatibleNativeConversationMessage, setNativeConversationMessage, } from '@/providers/conversation-metadata' -import { providerHistoryProtocols, requiresNativeToolHistory } from '@/providers/history-adapters' +import { getProviderHistoryProtocol, requiresNativeToolHistory } from '@/providers/history-adapters' import { executeProviderTool } from '@/providers/runtime-context' import { isAbortError } from '@/providers/streaming-tool-loop-shared' import type { Message, ProviderId, ProviderRequest } from '@/providers/types' @@ -139,7 +139,7 @@ export async function restoreConversationNativeMessages( providerId === 'azure-openai' && isChatCompletionsEndpoint(request?.azureEndpoint || env.AZURE_OPENAI_ENDPOINT || '') ? 'chat-completions' - : providerHistoryProtocols[providerId] + : getProviderHistoryProtocol(providerId, model) if (!protocol) throw new Error('Evaluation providers do not support conversation history') const restored: Message[] = [] for (const group of groupConversationMessages(messages)) { @@ -178,7 +178,7 @@ export async function restoreConversationNativeMessages( if ( first.tool_calls?.length && !getNativeConversationMessage(first, protocol) && - requiresNativeToolHistory(providerId) + requiresNativeToolHistory(providerId, model) ) { logger.info('Agent memory used portable execution history', { protocol }) restored.push(renderConversationExecutionRecord(group)) diff --git a/apps/sim/providers/history-adapters.test.ts b/apps/sim/providers/history-adapters.test.ts index 0af9c491b3a..45c0607fd6f 100644 --- a/apps/sim/providers/history-adapters.test.ts +++ b/apps/sim/providers/history-adapters.test.ts @@ -125,7 +125,7 @@ describe('canonical provider wire adapters', () => { expect( PROVIDER_DEFINITIONS[providerId].models.every((model) => isEvaluationModel(model.id)) ).toBe(true) - } else { + } else if (typeof protocol !== 'function') { expect(providerHistoryAdapters[protocol]).toBeDefined() } } diff --git a/apps/sim/providers/history-adapters.ts b/apps/sim/providers/history-adapters.ts index 07c1e70a341..6e7885cda52 100644 --- a/apps/sim/providers/history-adapters.ts +++ b/apps/sim/providers/history-adapters.ts @@ -3,6 +3,7 @@ import type { CapturedConversationStep, ConversationProtocol, } from '@/lib/memory/conversation-types' +import { getKieConversationProtocol, isKieClaudeModel } from '@/providers/kie/utils' import type { ProviderId } from '@/providers/types' export interface ProviderHistoryAdapter { @@ -80,7 +81,15 @@ export const providerHistoryAdapters: Record capture('bedrock', value) }, } -export const providerHistoryProtocols: Record = { +/** + * Native history protocol per provider. `null` marks evaluation-only providers; a function + * resolves per model for providers that proxy more than one wire protocol. Read it through + * {@link getProviderHistoryProtocol}. + */ +export const providerHistoryProtocols: Record< + ProviderId, + ConversationProtocol | null | ((model: string) => ConversationProtocol) +> = { openai: 'responses', 'azure-openai': 'responses', anthropic: 'anthropic', @@ -98,6 +107,7 @@ export const providerHistoryProtocols: Record => { + if (!request.apiKey) { + throw new Error('API key is required for Kie') + } + + if (isKieClaudeModel(request.model)) { + return executeAnthropicProviderRequest(request, { + providerId: 'kie', + providerLabel: 'Kie', + resolveWireModel: ({ model }) => getKieWireModel(model), + // Kie authenticates its Claude proxy with a bearer token, not `x-api-key`. + createClient: (apiKey) => + getCachedProviderClient( + `kie::${apiKey}`, + () => + new Anthropic({ + baseURL: KIE_CLAUDE_BASE_URL, + apiKey: null, + authToken: apiKey, + maxRetries: PROVIDER_MAX_RETRIES, + fetch: kieFetch, + }) + ), + logger, + }) + } + + const endpoint = getKieResponsesEndpoint(request.model) + if (!endpoint) { + throw new Error(`Kie does not serve model ${request.model}`) + } + + return executeResponsesProviderRequest(request, { + providerId: 'kie', + providerLabel: 'Kie', + modelName: getKieWireModel(request.model), + capabilityModel: request.model, + endpoint, + headers: { + Authorization: `Bearer ${request.apiKey}`, + 'Content-Type': 'application/json', + }, + logger, + fetch: kieFetch, + }) + }, +} diff --git a/apps/sim/providers/kie/transport.test.ts b/apps/sim/providers/kie/transport.test.ts new file mode 100644 index 00000000000..21f29541baa --- /dev/null +++ b/apps/sim/providers/kie/transport.test.ts @@ -0,0 +1,90 @@ +/** + * @vitest-environment node + */ +import { createMockResponse } from '@sim/testing' +import { describe, expect, it } from 'vitest' +import { createKieFetch } from '@/providers/kie/transport' + +const JSON_HEADERS = { 'content-type': 'application/json;charset=utf-8' } + +const KIE_UNAUTHORIZED = { + code: 401, + msg: 'Unauthorized – Authentication failed. Please check that your Authorization and Content-Type headers are correctly set.', +} + +function kieFetchReturning(response: Response): typeof fetch { + return createKieFetch(async () => response) +} + +describe('createKieFetch', () => { + it('turns an HTTP 200 error envelope into the HTTP status it reports', async () => { + const response = await kieFetchReturning( + createMockResponse({ json: KIE_UNAUTHORIZED, headers: JSON_HEADERS }) + )('https://api.kie.ai') + + expect(response.status).toBe(401) + expect(response.ok).toBe(false) + expect(await response.json()).toEqual({ + type: 'error', + error: { type: 'kie_error', message: KIE_UNAUTHORIZED.msg }, + }) + }) + + it.each([ + [429, 429], + [455, 503], + [505, 403], + [0, 502], + [9999, 502], + ])('maps Kie-specific code %i to HTTP %i', async (code, status) => { + const response = await kieFetchReturning( + createMockResponse({ json: { code, msg: 'failure' }, headers: JSON_HEADERS }) + )('https://api.kie.ai') + + expect(response.status).toBe(status) + }) + + it('passes a real Anthropic message through with its body intact', async () => { + const message = { + id: 'msg_1', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'hi' }], + } + const response = await kieFetchReturning( + createMockResponse({ json: message, headers: JSON_HEADERS }) + )('https://api.kie.ai') + + expect(response.status).toBe(200) + expect(await response.json()).toEqual(message) + }) + + it('passes a success envelope with code 200 through untouched', async () => { + const body = { code: 200, msg: 'success', data: {} } + const response = await kieFetchReturning( + createMockResponse({ json: body, headers: JSON_HEADERS }) + )('https://api.kie.ai') + + expect(response.status).toBe(200) + expect(await response.json()).toEqual(body) + }) + + it('never buffers a streaming response', async () => { + const stream = new Response('data: {"type":"response.created"}\n\n', { + headers: { 'content-type': 'text/event-stream' }, + }) + const response = await kieFetchReturning(stream)('https://api.kie.ai') + + expect(response).toBe(stream) + expect(stream.bodyUsed).toBe(false) + }) + + it('passes through a JSON body that is not valid JSON', async () => { + const response = await kieFetchReturning( + createMockResponse({ text: 'not json', headers: JSON_HEADERS }) + )('https://api.kie.ai') + + expect(response.status).toBe(200) + expect(await response.text()).toBe('not json') + }) +}) diff --git a/apps/sim/providers/kie/transport.ts b/apps/sim/providers/kie/transport.ts new file mode 100644 index 00000000000..b67803e7105 --- /dev/null +++ b/apps/sim/providers/kie/transport.ts @@ -0,0 +1,57 @@ +import { isRecordLike } from '@sim/utils/object' + +/** + * Kie codes with no matching HTTP meaning, mapped so the SDKs' retry policy treats + * them correctly: 455 (service unavailable) and 501 (generation failed) are transient, + * 505 (feature disabled) is not. + */ +const KIE_CODE_TO_HTTP_STATUS: Record = { + 455: 503, + 501: 502, + 505: 403, +} + +function toHttpStatus(code: number): number { + const mapped = KIE_CODE_TO_HTTP_STATUS[code] + if (mapped) return mapped + return code >= 400 && code <= 599 ? code : 502 +} + +/** + * Wraps `fetch` so Kie errors surface as HTTP errors. + * + * Most Kie chat routes answer failures (bad key, no credits, rate limit) with + * HTTP 200 and a `{ code, msg }` body. The Anthropic SDK and the Responses core + * would read that as a successful, empty completion, so this rewrites it into a + * real error response carrying Kie's code and message. Streams are never + * buffered: a failed streaming request comes back as JSON, not `text/event-stream`. + */ +export function createKieFetch(baseFetch?: typeof fetch): typeof fetch { + return async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const response = await (baseFetch ?? fetch)(input, init) + const contentType = response.headers.get('content-type') ?? '' + if (response.status !== 200 || !contentType.includes('application/json')) return response + + const text = await response.text() + let body: unknown + try { + body = JSON.parse(text) + } catch { + body = undefined + } + + if (!isRecordLike(body) || typeof body.code !== 'number' || body.code === 200) { + return new Response(text, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }) + } + + const message = typeof body.msg === 'string' ? body.msg : `Kie request failed (${body.code})` + return new Response(JSON.stringify({ type: 'error', error: { type: 'kie_error', message } }), { + status: toHttpStatus(body.code), + headers: { 'content-type': 'application/json' }, + }) + } +} diff --git a/apps/sim/providers/kie/utils.ts b/apps/sim/providers/kie/utils.ts new file mode 100644 index 00000000000..78bb774787f --- /dev/null +++ b/apps/sim/providers/kie/utils.ts @@ -0,0 +1,43 @@ +import type { ConversationProtocol } from '@/lib/memory/conversation-types' + +export const KIE_API_BASE_URL = 'https://api.kie.ai' + +/** Anthropic Messages-compatible base URL; the SDK appends `/v1/messages`. */ +export const KIE_CLAUDE_BASE_URL = `${KIE_API_BASE_URL}/claude` + +/** + * Kie serves each model family from its own Responses-compatible path, and a + * model is rejected on any path but its own. + */ +const KIE_RESPONSES_PATHS: Record = { + 'gpt-6-astra': '/codex/v1/responses', + 'gpt-6-sol': '/codex/v1/responses', + 'gpt-6-luna': '/codex/v1/responses', + 'gpt-5-6-sol': '/codex/v1/responses', + 'gpt-5-6-terra': '/codex/v1/responses', + 'gpt-5-6-luna': '/codex/v1/responses', + 'gpt-5-5': '/codex/v1/responses', + 'grok-4-6': '/grok/v1/responses', + 'grok-4-5': '/grok/v1/responses', + 'grok-4-3': '/grok/v1/responses', + 'kimi-k3': '/openai/v1/responses', +} + +/** Strips the `kie/` catalog prefix to get the model slug Kie expects on the wire. */ +export function getKieWireModel(model: string): string { + return model.replace(/^kie\//i, '') +} + +/** Claude models go through Kie's Anthropic Messages proxy; everything else is Responses. */ +export function isKieClaudeModel(model: string): boolean { + return getKieWireModel(model).toLowerCase().startsWith('claude-') +} + +export function getKieResponsesEndpoint(model: string): string | undefined { + const path = KIE_RESPONSES_PATHS[getKieWireModel(model).toLowerCase()] + return path ? `${KIE_API_BASE_URL}${path}` : undefined +} + +export function getKieConversationProtocol(model: string): ConversationProtocol { + return isKieClaudeModel(model) ? 'anthropic' : 'responses' +} diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index cc0c7a14386..dfd4a70511a 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -18,6 +18,7 @@ import { FireworksIcon, GeminiIcon, GroqIcon, + KieIcon, KimiIcon, LitellmIcon, MetaIcon, @@ -3697,6 +3698,435 @@ export const PROVIDER_DEFINITIONS: Record = { }, ], }, + kie: { + id: 'kie', + name: 'Kie', + description: 'Claude, GPT, Grok, and Kimi models through the Kie.ai API', + defaultModel: 'kie/claude-opus-5-5', + modelPatterns: [/^kie\//], + icon: KieIcon, + color: '#1D4ED8', + isReseller: true, + capabilities: { + toolUsageControl: true, + }, + models: [ + { + id: 'kie/claude-opus-5-5', + pricing: { + input: 1.6, + output: 8.0, + updatedAt: '2026-09-28', + }, + capabilities: { + forcedToolUse: false, + maxOutputTokens: 128000, + thinking: { + levels: ['low', 'medium', 'high', 'xhigh', 'max'], + default: 'medium', + streamed: 'summary', + }, + }, + contextWindow: 1000000, + releaseDate: '2026-09-22', + recommended: true, + }, + { + id: 'kie/claude-opus-5', + pricing: { + input: 2.0, + output: 10.0, + updatedAt: '2026-09-28', + }, + capabilities: { + maxOutputTokens: 128000, + thinking: { + levels: ['low', 'medium', 'high', 'xhigh', 'max'], + default: 'high', + streamed: 'summary', + }, + }, + contextWindow: 1000000, + releaseDate: '2026-07-24', + sunset: { status: 'legacy' }, + }, + { + id: 'kie/claude-fable-5', + pricing: { + input: 4.0, + output: 20.0, + updatedAt: '2026-09-28', + }, + capabilities: { + maxOutputTokens: 128000, + thinking: { + levels: ['low', 'medium', 'high', 'xhigh', 'max'], + default: 'high', + streamed: 'summary', + }, + }, + contextWindow: 1000000, + releaseDate: '2026-06-09', + sunset: { status: 'legacy' }, + }, + { + id: 'kie/claude-sonnet-5', + pricing: { + input: 0.85, + output: 4.275, + updatedAt: '2026-09-28', + }, + capabilities: { + maxOutputTokens: 128000, + thinking: { + levels: ['low', 'medium', 'high', 'xhigh', 'max'], + default: 'high', + streamed: 'summary', + }, + }, + contextWindow: 1000000, + releaseDate: '2026-06-30', + sunset: { status: 'legacy' }, + }, + { + id: 'kie/claude-opus-4-8', + pricing: { + input: 2.0, + output: 10.0, + updatedAt: '2026-09-28', + }, + capabilities: { + maxOutputTokens: 128000, + thinking: { + levels: ['low', 'medium', 'high', 'xhigh', 'max'], + default: 'high', + streamed: 'summary', + }, + }, + contextWindow: 1000000, + releaseDate: '2026-05-28', + sunset: { status: 'legacy' }, + }, + { + id: 'kie/claude-opus-4-7', + pricing: { + input: 1.425, + output: 7.15, + updatedAt: '2026-09-28', + }, + capabilities: { + maxOutputTokens: 128000, + thinking: { + levels: ['low', 'medium', 'high', 'xhigh', 'max'], + default: 'high', + streamed: 'summary', + }, + }, + contextWindow: 1000000, + releaseDate: '2026-04-16', + sunset: { status: 'legacy' }, + }, + { + id: 'kie/claude-opus-4-6', + pricing: { + input: 1.425, + output: 7.15, + updatedAt: '2026-09-28', + }, + capabilities: { + temperature: { + min: 0, + max: 1, + }, + maxOutputTokens: 128000, + thinking: { + levels: ['low', 'medium', 'high', 'max'], + default: 'high', + streamed: 'summary', + }, + }, + contextWindow: 1000000, + releaseDate: '2026-02-05', + sunset: { status: 'legacy' }, + }, + { + id: 'kie/claude-sonnet-4-6', + pricing: { + input: 0.85, + output: 4.275, + updatedAt: '2026-09-28', + }, + capabilities: { + temperature: { + min: 0, + max: 1, + }, + maxOutputTokens: 128000, + thinking: { + levels: ['low', 'medium', 'high', 'max'], + default: 'high', + streamed: 'summary', + }, + }, + contextWindow: 1000000, + releaseDate: '2026-02-17', + sunset: { status: 'legacy' }, + }, + { + id: 'kie/claude-opus-4-5', + pricing: { + input: 1.425, + output: 7.15, + updatedAt: '2026-09-28', + }, + capabilities: { + temperature: { + min: 0, + max: 1, + }, + maxOutputTokens: 64000, + thinking: { + levels: ['low', 'medium', 'high'], + default: 'high', + streamed: 'summary', + }, + }, + contextWindow: 200000, + releaseDate: '2025-11-24', + sunset: { status: 'legacy' }, + }, + { + id: 'kie/claude-sonnet-4-5', + pricing: { + input: 0.85, + output: 4.275, + updatedAt: '2026-09-28', + }, + capabilities: { + temperature: { + min: 0, + max: 1, + }, + maxOutputTokens: 64000, + thinking: { + levels: ['low', 'medium', 'high'], + default: 'high', + streamed: 'summary', + }, + }, + contextWindow: 200000, + releaseDate: '2025-09-29', + sunset: { status: 'legacy' }, + }, + { + id: 'kie/claude-haiku-4-5', + pricing: { + input: 0.275, + output: 1.425, + updatedAt: '2026-09-28', + }, + capabilities: { + temperature: { + min: 0, + max: 1, + }, + maxOutputTokens: 64000, + thinking: { + levels: ['low', 'medium', 'high'], + default: 'high', + streamed: 'summary', + }, + }, + contextWindow: 200000, + releaseDate: '2025-10-15', + }, + { + id: 'kie/gpt-6-astra', + pricing: { + input: 2.8, + cachedInput: 0.28, + output: 14.0, + updatedAt: '2026-09-28', + }, + capabilities: { + reasoningEffort: { + values: ['low', 'medium', 'high', 'xhigh'], + }, + maxOutputTokens: 128000, + }, + contextWindow: 1050000, + releaseDate: '2026-09-03', + recommended: true, + }, + { + id: 'kie/gpt-6-sol', + pricing: { + input: 0.6, + cachedInput: 0.06, + output: 3.0, + updatedAt: '2026-09-28', + }, + capabilities: { + reasoningEffort: { + values: ['low', 'medium', 'high', 'xhigh'], + }, + maxOutputTokens: 128000, + }, + contextWindow: 1050000, + releaseDate: '2026-09-22', + }, + { + id: 'kie/gpt-6-luna', + pricing: { + input: 0.03, + cachedInput: 0.003, + output: 0.15, + updatedAt: '2026-09-28', + }, + capabilities: { + reasoningEffort: { + values: ['low', 'medium', 'high', 'xhigh'], + }, + maxOutputTokens: 128000, + }, + contextWindow: 1050000, + releaseDate: '2026-09-22', + }, + { + id: 'kie/gpt-5-6-sol', + pricing: { + input: 1.4, + cachedInput: 0.14, + output: 8.4, + updatedAt: '2026-09-28', + }, + capabilities: { + reasoningEffort: { + values: ['low', 'medium', 'high', 'xhigh'], + }, + maxOutputTokens: 128000, + }, + contextWindow: 1050000, + releaseDate: '2026-07-09', + }, + { + id: 'kie/gpt-5-6-terra', + pricing: { + input: 0.56, + cachedInput: 0.056, + output: 3.36, + updatedAt: '2026-09-28', + }, + capabilities: { + reasoningEffort: { + values: ['low', 'medium', 'high', 'xhigh'], + }, + maxOutputTokens: 128000, + }, + contextWindow: 1050000, + releaseDate: '2026-07-09', + }, + { + id: 'kie/gpt-5-6-luna', + pricing: { + input: 0.056, + cachedInput: 0.0056, + output: 0.336, + updatedAt: '2026-09-28', + }, + capabilities: { + reasoningEffort: { + values: ['low', 'medium', 'high', 'xhigh'], + }, + maxOutputTokens: 128000, + }, + contextWindow: 1050000, + releaseDate: '2026-07-09', + }, + { + id: 'kie/gpt-5-5', + pricing: { + input: 1.4, + cachedInput: 0.14, + output: 8.4, + updatedAt: '2026-09-28', + }, + capabilities: { + reasoningEffort: { + values: ['low', 'medium', 'high', 'xhigh'], + }, + maxOutputTokens: 128000, + }, + contextWindow: 1050000, + releaseDate: '2026-04-23', + }, + { + id: 'kie/grok-4-6', + pricing: { + input: 0.8, + cachedInput: 0.2, + output: 2.4, + updatedAt: '2026-09-28', + }, + capabilities: { + reasoningEffort: { + values: ['low', 'medium', 'high', 'xhigh'], + }, + }, + contextWindow: 500000, + releaseDate: '2026-08-12', + }, + { + id: 'kie/grok-4-5', + pricing: { + input: 0.8, + cachedInput: 0.12, + output: 2.4, + updatedAt: '2026-09-28', + }, + capabilities: { + reasoningEffort: { + values: ['low', 'medium', 'high', 'xhigh'], + }, + }, + contextWindow: 500000, + releaseDate: '2026-07-08', + }, + { + id: 'kie/grok-4-3', + pricing: { + input: 0.5, + cachedInput: 0.08, + output: 1.0, + updatedAt: '2026-09-28', + }, + capabilities: { + reasoningEffort: { + values: ['low', 'medium', 'high', 'xhigh'], + }, + }, + contextWindow: 1000000, + releaseDate: '2026-04-30', + }, + { + id: 'kie/kimi-k3', + pricing: { + input: 2.4, + cachedInput: 0.24, + output: 12.0, + updatedAt: '2026-09-28', + }, + capabilities: { + reasoningEffort: { + values: ['low', 'medium', 'high'], + }, + maxOutputTokens: 1048576, + }, + contextWindow: 1048576, + releaseDate: '2026-07-16', + }, + ], + }, zai: { id: 'zai', name: 'Z.ai', @@ -5822,6 +6252,8 @@ const PROVIDER_THINKING_STREAM_DEFAULTS: Record logger: Logger @@ -183,8 +188,14 @@ export async function executeResponsesProviderRequest( const initialInput = buildResponsesInputFromMessages(allMessages, config.providerId) + /** + * `stream` is always explicit: OpenAI defaults it to false, but Responses-compatible + * resellers (Kie) default it to true and would answer a non-streaming call with SSE. + * Streaming calls override it per request. + */ const basePayload: Record = { model: config.modelName, + stream: false, } /** @@ -211,7 +222,7 @@ export async function executeResponsesProviderRequest( * organization verification; see the strip-and-retry fallback in the * request helpers below. */ - if (supportsReasoningEffort(config.modelName)) { + if (supportsReasoningEffort(config.capabilityModel ?? config.modelName)) { if (isProviderConversationCaptureEnabled(request)) { basePayload.include = ['reasoning.encrypted_content'] } diff --git a/apps/sim/providers/registry.ts b/apps/sim/providers/registry.ts index 0088ac0f2ac..0a1b0da95f3 100644 --- a/apps/sim/providers/registry.ts +++ b/apps/sim/providers/registry.ts @@ -9,6 +9,7 @@ import { deepseekProvider } from '@/providers/deepseek' import { fireworksProvider } from '@/providers/fireworks' import { googleProvider } from '@/providers/google' import { groqProvider } from '@/providers/groq' +import { kieProvider } from '@/providers/kie' import { kimiProvider } from '@/providers/kimi' import { litellmProvider } from '@/providers/litellm' import { metaProvider } from '@/providers/meta' @@ -45,6 +46,7 @@ const providerRegistry: Record = { meta: metaProvider, zai: zaiProvider, kimi: kimiProvider, + kie: kieProvider, vllm: vllmProvider, litellm: litellmProvider, mistral: mistralProvider, diff --git a/apps/sim/providers/types.ts b/apps/sim/providers/types.ts index 2cda6e7377c..d602c275649 100644 --- a/apps/sim/providers/types.ts +++ b/apps/sim/providers/types.ts @@ -19,6 +19,7 @@ export type ProviderId = | 'meta' | 'zai' | 'kimi' + | 'kie' | 'mistral' | 'ollama' | 'ollama-cloud' diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 54b23cbec7f..fe5d6baab85 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -163,6 +163,7 @@ export const providers: Record = { meta: buildProviderMetadata('meta'), zai: buildProviderMetadata('zai'), kimi: buildProviderMetadata('kimi'), + kie: buildProviderMetadata('kie'), mistral: buildProviderMetadata('mistral'), bedrock: buildProviderMetadata('bedrock'), openrouter: buildProviderMetadata('openrouter'), diff --git a/apps/sim/tools/types.ts b/apps/sim/tools/types.ts index c26205d140f..309928ca9a9 100644 --- a/apps/sim/tools/types.ts +++ b/apps/sim/tools/types.ts @@ -19,6 +19,7 @@ export type BYOKProviderId = | 'together' | 'baseten' | 'ollama-cloud' + | 'kie' | 'falai' | 'firecrawl' | 'exa' From 40678268279bf18bb1399a983ec5e821049ef9df Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 28 Sep 2026 14:54:54 -0700 Subject: [PATCH 2/4] improvement(providers): use the official Kie logo and list Kie as a streaming tool-call provider --- apps/docs/components/icons.tsx | 36 ++++++------------- .../content/docs/workflows/blocks/agent.mdx | 2 +- apps/sim/components/icons.tsx | 36 ++++++------------- .../providers/streaming-tool-loop-shared.ts | 1 + 4 files changed, 22 insertions(+), 53 deletions(-) diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 263a13678ba..53cb7eb66b8 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4327,39 +4327,23 @@ export function KieIcon(props: SVGProps) { Kie - - - + + + + - - - - - - - - - + ) } diff --git a/apps/docs/content/docs/workflows/blocks/agent.mdx b/apps/docs/content/docs/workflows/blocks/agent.mdx index a9494bd2751..872f0a07bb9 100644 --- a/apps/docs/content/docs/workflows/blocks/agent.mdx +++ b/apps/docs/content/docs/workflows/blocks/agent.mdx @@ -154,7 +154,7 @@ While an agent runs, Sim can stream its thinking and tool lifecycle live — the {/* agent-stream-capabilities:begin — generated by `bun run agent-stream-docs:generate`; do not edit between markers */} -Live tool-call chips stream for **OpenAI, Anthropic, Azure Anthropic, Google, Vertex AI, DeepSeek, Groq, AWS Bedrock** models. Other providers run tools without live chips and project the settled final answer when the run completes; they do not ask the model to regenerate that answer just to create a stream. +Live tool-call chips stream for **OpenAI, Anthropic, Azure Anthropic, Google, Vertex AI, DeepSeek, Groq, Kie, AWS Bedrock** models. Other providers run tools without live chips and project the settled final answer when the run completes; they do not ask the model to regenerate that answer just to create a stream. | Provider | Streamed thinking | Models | |----------|-------------------|--------| diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 263a13678ba..53cb7eb66b8 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -4327,39 +4327,23 @@ export function KieIcon(props: SVGProps) { Kie - - - + + + + - - - - - - - - - + ) } diff --git a/apps/sim/providers/streaming-tool-loop-shared.ts b/apps/sim/providers/streaming-tool-loop-shared.ts index 5ac0c49d777..50781f79e59 100644 --- a/apps/sim/providers/streaming-tool-loop-shared.ts +++ b/apps/sim/providers/streaming-tool-loop-shared.ts @@ -20,6 +20,7 @@ export const STREAMING_TOOL_CALL_PROVIDERS: ReadonlySet = new Set([ 'openai', 'anthropic', 'azure-anthropic', + 'kie', 'groq', 'deepseek', 'google', From 97381a8c0c3f67869a66357f3f6fb60f6d09c0d2 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 28 Sep 2026 15:07:09 -0700 Subject: [PATCH 3/4] fix(providers): keep Kie retry headers on rewritten errors and cover the request contract --- apps/sim/providers/kie/index.test.ts | 99 ++++++++++++++++++++++++ apps/sim/providers/kie/transport.test.ts | 18 +++++ apps/sim/providers/kie/transport.ts | 18 ++++- 3 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 apps/sim/providers/kie/index.test.ts diff --git a/apps/sim/providers/kie/index.test.ts b/apps/sim/providers/kie/index.test.ts new file mode 100644 index 00000000000..eba264c3cde --- /dev/null +++ b/apps/sim/providers/kie/index.test.ts @@ -0,0 +1,99 @@ +/** + * @vitest-environment node + * + * Wire contract with Kie's two request paths, as observed against the live API: + * bearer auth on both, `stream` sent explicitly (Kie treats an absent flag as + * true), Kie's own model slugs, and HTTP-200 error envelopes surfacing as errors. + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { kieProvider } from '@/providers/kie' + +const fetchMock = vi.fn() + +const CLAUDE_MESSAGE = { + id: 'msg_1', + type: 'message', + role: 'assistant', + model: 'claude-opus-5-5', + content: [{ type: 'text', text: 'pong' }], + stop_reason: 'end_turn', + stop_sequence: null, + usage: { input_tokens: 3, output_tokens: 1 }, +} + +const RESPONSES_RESULT = { + id: 'resp_1', + object: 'response', + status: 'completed', + output: [ + { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'pong' }] }, + ], + usage: { input_tokens: 3, output_tokens: 1, total_tokens: 4 }, +} + +function sentRequest(): { url: string; headers: Headers; body: Record } { + const [input, init] = fetchMock.mock.calls[0] + return { + url: String(input), + headers: new Headers(init?.headers), + body: JSON.parse(String(init?.body)), + } +} + +describe('kieProvider', () => { + beforeEach(() => { + fetchMock.mockReset() + vi.stubGlobal('fetch', fetchMock) + }) + + it('sends Claude models to the Messages proxy with bearer auth and an explicit stream flag', async () => { + fetchMock.mockResolvedValue(Response.json(CLAUDE_MESSAGE)) + + const result = await kieProvider.executeRequest({ + model: 'kie/claude-opus-5-5', + apiKey: 'kie-key', + maxTokens: 1024, + messages: [{ role: 'user', content: 'ping' }], + }) + + const { url, headers, body } = sentRequest() + expect(url).toBe('https://api.kie.ai/claude/v1/messages') + expect(headers.get('authorization')).toBe('Bearer kie-key') + expect(headers.has('x-api-key')).toBe(false) + expect(body.model).toBe('claude-opus-5-5') + expect(body.stream).toBe(false) + expect(result).toMatchObject({ content: 'pong' }) + }) + + it('surfaces an HTTP 200 error envelope on the Claude route as an error', async () => { + fetchMock.mockResolvedValue(Response.json({ code: 402, msg: 'Insufficient credits' })) + + await expect( + kieProvider.executeRequest({ + model: 'kie/claude-opus-5-5', + apiKey: 'kie-key', + maxTokens: 1024, + messages: [{ role: 'user', content: 'ping' }], + }) + ).rejects.toThrow('Insufficient credits') + }) + + it('sends Responses models to their family endpoint with reasoning effort', async () => { + fetchMock.mockResolvedValue(Response.json(RESPONSES_RESULT)) + + const result = await kieProvider.executeRequest({ + model: 'kie/grok-4-6', + apiKey: 'kie-key', + reasoningEffort: 'high', + messages: [{ role: 'user', content: 'ping' }], + }) + + const { url, headers, body } = sentRequest() + expect(url).toBe('https://api.kie.ai/grok/v1/responses') + expect(headers.get('authorization')).toBe('Bearer kie-key') + expect(body.model).toBe('grok-4-6') + expect(body.stream).toBe(false) + expect(body.reasoning).toMatchObject({ effort: 'high' }) + expect(result).toMatchObject({ content: 'pong' }) + }) +}) diff --git a/apps/sim/providers/kie/transport.test.ts b/apps/sim/providers/kie/transport.test.ts index 21f29541baa..71b3c8fcc42 100644 --- a/apps/sim/providers/kie/transport.test.ts +++ b/apps/sim/providers/kie/transport.test.ts @@ -30,6 +30,24 @@ describe('createKieFetch', () => { }) }) + it('keeps retry and tracing headers on a rewritten error', async () => { + const response = await kieFetchReturning( + createMockResponse({ + json: { code: 429, msg: 'Rate limited' }, + headers: { + ...JSON_HEADERS, + 'retry-after': '7', + 'x-request-id': 'req_1', + 'content-length': '999', + }, + }) + )('https://api.kie.ai') + + expect(response.headers.get('retry-after')).toBe('7') + expect(response.headers.get('x-request-id')).toBe('req_1') + expect(response.headers.get('content-length')).toBeNull() + }) + it.each([ [429, 429], [455, 503], diff --git a/apps/sim/providers/kie/transport.ts b/apps/sim/providers/kie/transport.ts index b67803e7105..c0f2c435341 100644 --- a/apps/sim/providers/kie/transport.ts +++ b/apps/sim/providers/kie/transport.ts @@ -40,18 +40,32 @@ export function createKieFetch(baseFetch?: typeof fetch): typeof fetch { body = undefined } + const headers = rebodiedHeaders(response.headers) if (!isRecordLike(body) || typeof body.code !== 'number' || body.code === 200) { return new Response(text, { status: response.status, statusText: response.statusText, - headers: response.headers, + headers, }) } const message = typeof body.msg === 'string' ? body.msg : `Kie request failed (${body.code})` + headers.set('content-type', 'application/json') return new Response(JSON.stringify({ type: 'error', error: { type: 'kie_error', message } }), { status: toHttpStatus(body.code), - headers: { 'content-type': 'application/json' }, + headers, }) } } + +/** + * Upstream headers for a body that has already been read and decoded. `Retry-After` + * and request ids must survive for the retry policy and diagnostics; the length and + * encoding no longer describe the new body. + */ +function rebodiedHeaders(upstream: Headers): Headers { + const headers = new Headers(upstream) + headers.delete('content-length') + headers.delete('content-encoding') + return headers +} From 1859f19ce25f2e2da680d218f36950c4f9043e4a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 28 Sep 2026 15:34:02 -0700 Subject: [PATCH 4/4] fix(providers): parse untyped Kie JSON responses and match content types case-insensitively --- apps/sim/providers/kie/index.test.ts | 15 ++++++++++++ apps/sim/providers/kie/transport.test.ts | 29 ++++++++++++++++++++++++ apps/sim/providers/kie/transport.ts | 15 ++++++++---- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/apps/sim/providers/kie/index.test.ts b/apps/sim/providers/kie/index.test.ts index eba264c3cde..6bcf060f72c 100644 --- a/apps/sim/providers/kie/index.test.ts +++ b/apps/sim/providers/kie/index.test.ts @@ -65,6 +65,21 @@ describe('kieProvider', () => { expect(result).toMatchObject({ content: 'pong' }) }) + it('parses a Claude message Kie returns without a content type', async () => { + fetchMock.mockResolvedValue( + new Response(new TextEncoder().encode(JSON.stringify(CLAUDE_MESSAGE))) + ) + + const result = await kieProvider.executeRequest({ + model: 'kie/claude-opus-5-5', + apiKey: 'kie-key', + maxTokens: 1024, + messages: [{ role: 'user', content: 'ping' }], + }) + + expect(result).toMatchObject({ content: 'pong' }) + }) + it('surfaces an HTTP 200 error envelope on the Claude route as an error', async () => { fetchMock.mockResolvedValue(Response.json({ code: 402, msg: 'Insufficient credits' })) diff --git a/apps/sim/providers/kie/transport.test.ts b/apps/sim/providers/kie/transport.test.ts index 71b3c8fcc42..6a1cff867ec 100644 --- a/apps/sim/providers/kie/transport.test.ts +++ b/apps/sim/providers/kie/transport.test.ts @@ -48,6 +48,35 @@ describe('createKieFetch', () => { expect(response.headers.get('content-length')).toBeNull() }) + it('matches the JSON content type case-insensitively', async () => { + const response = await kieFetchReturning( + createMockResponse({ + json: KIE_UNAUTHORIZED, + headers: { 'content-type': 'Application/JSON; charset=UTF-8' }, + }) + )('https://api.kie.ai') + + expect(response.status).toBe(401) + }) + + it('treats an untyped JSON body as JSON and labels it', async () => { + const message = { id: 'msg_1', type: 'message', content: [{ type: 'text', text: 'hi' }] } + const response = await kieFetchReturning( + new Response(new TextEncoder().encode(JSON.stringify(message))) + )('https://api.kie.ai') + + expect(response.headers.get('content-type')).toBe('application/json') + expect(await response.json()).toEqual(message) + }) + + it('detects an error envelope in an untyped body', async () => { + const response = await kieFetchReturning( + new Response(new TextEncoder().encode(JSON.stringify(KIE_UNAUTHORIZED))) + )('https://api.kie.ai') + + expect(response.status).toBe(401) + }) + it.each([ [429, 429], [455, 503], diff --git a/apps/sim/providers/kie/transport.ts b/apps/sim/providers/kie/transport.ts index c0f2c435341..451f5b22f13 100644 --- a/apps/sim/providers/kie/transport.ts +++ b/apps/sim/providers/kie/transport.ts @@ -18,19 +18,23 @@ function toHttpStatus(code: number): number { } /** - * Wraps `fetch` so Kie errors surface as HTTP errors. + * Wraps `fetch` so Kie responses read like the APIs they proxy. * * Most Kie chat routes answer failures (bad key, no credits, rate limit) with * HTTP 200 and a `{ code, msg }` body. The Anthropic SDK and the Responses core * would read that as a successful, empty completion, so this rewrites it into a - * real error response carrying Kie's code and message. Streams are never - * buffered: a failed streaming request comes back as JSON, not `text/event-stream`. + * real error response carrying Kie's code and message. Kie also sends some JSON + * bodies with no content type, which the Anthropic SDK would parse as text; those + * are labeled as JSON. Streams are never buffered: they are typed + * `text/event-stream`, and a failed streaming request comes back as JSON. */ export function createKieFetch(baseFetch?: typeof fetch): typeof fetch { return async (input: RequestInfo | URL, init?: RequestInit): Promise => { const response = await (baseFetch ?? fetch)(input, init) - const contentType = response.headers.get('content-type') ?? '' - if (response.status !== 200 || !contentType.includes('application/json')) return response + const contentType = response.headers.get('content-type')?.toLowerCase() + if (response.status !== 200 || (contentType && !contentType.includes('application/json'))) { + return response + } const text = await response.text() let body: unknown @@ -41,6 +45,7 @@ export function createKieFetch(baseFetch?: typeof fetch): typeof fetch { } const headers = rebodiedHeaders(response.headers) + if (body !== undefined && !contentType) headers.set('content-type', 'application/json') if (!isRecordLike(body) || typeof body.code !== 'number' || body.code === 200) { return new Response(text, { status: response.status,