Skip to content

Commit fda057c

Browse files
committed
Merge branch 'sfierro/specs' into sfierro/specs_server_apis
2 parents 63a8e20 + 85568dc commit fda057c

File tree

20 files changed

+1008
-682
lines changed

20 files changed

+1008
-682
lines changed

app/desktop/studio_server/provider_api.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,11 +1140,17 @@ async def connect_bedrock(key_data: dict):
11401140

11411141
async def connect_kiln_copilot(key: str):
11421142
base_url = os.environ.get("KILN_SERVER_BASE_URL", "https://api.kiln.tech")
1143-
async with httpx.AsyncClient() as client:
1144-
response = await client.get(
1145-
f"{base_url}/v1/verify_api_key",
1146-
headers={"Authorization": f"Bearer {key}"},
1147-
timeout=20,
1143+
try:
1144+
async with httpx.AsyncClient() as client:
1145+
response = await client.get(
1146+
f"{base_url}/v1/verify_api_key",
1147+
headers={"Authorization": f"Bearer {key}"},
1148+
timeout=20,
1149+
)
1150+
except httpx.RequestError as e:
1151+
return JSONResponse(
1152+
status_code=400,
1153+
content={"message": f"Failed to connect to Kiln Copilot. Error: {e!s}"},
11481154
)
11491155

11501156
if response.status_code == 200:

app/desktop/studio_server/test_provider_api.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,35 @@ def test_connect_api_key_kiln_copilot_empty_key(client):
165165
assert response.json() == {"detail": "API Key not found"}
166166

167167

168+
@patch("app.desktop.studio_server.provider_api.httpx.AsyncClient.get")
169+
def test_connect_api_key_kiln_copilot_failure(mock_httpx_get, client):
170+
mock_response = MagicMock()
171+
mock_response.status_code = 401
172+
mock_response.json.return_value = {"message": "Invalid API key"}
173+
mock_httpx_get.return_value = mock_response
174+
175+
response = client.post(
176+
"/api/provider/connect_api_key",
177+
json={"provider": "kiln_copilot", "key_data": {"API Key": "invalid_key"}},
178+
)
179+
180+
assert response.status_code == 401
181+
assert response.json() == {"message": "Invalid API key"}
182+
183+
184+
@patch("app.desktop.studio_server.provider_api.httpx.AsyncClient.get")
185+
def test_connect_api_key_kiln_copilot_network_error(mock_httpx_get, client):
186+
mock_httpx_get.side_effect = httpx.RequestError("Network error")
187+
188+
response = client.post(
189+
"/api/provider/connect_api_key",
190+
json={"provider": "kiln_copilot", "key_data": {"API Key": "test_key"}},
191+
)
192+
193+
assert response.status_code == 400
194+
assert "Failed to connect" in response.json()["message"]
195+
196+
168197
@patch("app.desktop.studio_server.provider_api.requests.get")
169198
@patch("app.desktop.studio_server.provider_api.Config.shared")
170199
def test_connect_openai_success(mock_config_shared, mock_requests_get, client):

app/web_ui/src/lib/ui/dialog.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
disabled?: boolean
2424
loading?: boolean
2525
hide?: boolean
26+
width?: "normal" | "wide"
2627
}
2728
export let action_buttons: ActionButton[] = []
2829
let action_running = false
@@ -187,9 +188,9 @@
187188
</form>
188189
{:else}
189190
<button
190-
class="btn btn-sm h-10 min-w-24 {button.isPrimary
191-
? 'btn-primary'
192-
: ''}
191+
class="btn btn-sm h-10 min-w-24 {button.width === 'wide'
192+
? 'w-full'
193+
: ''} {button.isPrimary ? 'btn-primary' : ''}
193194
{button.isError ? 'btn-error' : ''}
194195
{button.isWarning ? 'btn-warning' : ''}"
195196
disabled={button.disabled || button.loading}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<script lang="ts">
2+
import { onMount } from "svelte"
3+
import createKindeClient from "@kinde-oss/kinde-auth-pkce-js"
4+
import { base_url } from "$lib/api_client"
5+
import posthog from "posthog-js"
6+
import { env } from "$env/dynamic/public"
7+
8+
export let onSuccess: () => void
9+
export let showTitle = true
10+
export let showCheckmark = false
11+
12+
let kindeClient: Awaited<ReturnType<typeof createKindeClient>> | null = null
13+
let apiKey = ""
14+
let apiKeyError = false
15+
let apiKeyMessage: string | null = null
16+
let submitting = false
17+
18+
const KINDE_ACCOUNT_DOMAIN =
19+
env.PUBLIC_KINDE_ACCOUNT_DOMAIN || "https://account.kiln.tech"
20+
const KINDE_ACCOUNT_CLIENT_ID =
21+
env.PUBLIC_KINDE_ACCOUNT_CLIENT_ID || "2428f47a1e0b404b82e68400a2d580c6"
22+
23+
async function initKindeClient() {
24+
if (kindeClient) return kindeClient
25+
26+
kindeClient = await createKindeClient({
27+
client_id: KINDE_ACCOUNT_CLIENT_ID,
28+
domain: KINDE_ACCOUNT_DOMAIN,
29+
redirect_uri: window.location.origin + window.location.pathname,
30+
on_redirect_callback: () => {},
31+
})
32+
33+
return kindeClient
34+
}
35+
36+
async function openSignup() {
37+
try {
38+
const kinde = await initKindeClient()
39+
if (!kinde) {
40+
apiKeyError = true
41+
apiKeyMessage = "Kinde configuration is missing"
42+
return
43+
}
44+
45+
await kinde.register()
46+
} catch (e) {
47+
console.error("openSignup error", e)
48+
apiKeyError = true
49+
apiKeyMessage = "Failed to open Kiln Copilot signup"
50+
}
51+
}
52+
53+
async function openSelfServePortal() {
54+
try {
55+
const kinde = await initKindeClient()
56+
if (!kinde) {
57+
apiKeyError = true
58+
apiKeyMessage = "Please sign up first"
59+
return
60+
}
61+
62+
const accessToken = await kinde.getToken()
63+
if (!accessToken) {
64+
apiKeyError = true
65+
apiKeyMessage = "Please sign up first before accessing the portal"
66+
return
67+
}
68+
69+
const response = await fetch(
70+
`${KINDE_ACCOUNT_DOMAIN}/account_api/v1/portal_link`,
71+
{
72+
headers: {
73+
Authorization: `Bearer ${accessToken}`,
74+
},
75+
},
76+
)
77+
78+
if (!response.ok) {
79+
throw new Error("Failed to generate portal link")
80+
}
81+
82+
const data = await response.json()
83+
window.open(data.url, "_blank")
84+
} catch (e) {
85+
console.error("openSelfServePortal error", e)
86+
apiKeyError = true
87+
apiKeyMessage =
88+
"Failed to open self-serve portal. Please try signing up first."
89+
}
90+
}
91+
92+
export async function submitApiKey() {
93+
if (!apiKey.trim()) {
94+
apiKeyError = true
95+
return false
96+
}
97+
98+
apiKeyError = false
99+
apiKeyMessage = null
100+
submitting = true
101+
102+
try {
103+
const res = await fetch(base_url + "/api/provider/connect_api_key", {
104+
method: "POST",
105+
headers: {
106+
"Content-Type": "application/json",
107+
},
108+
body: JSON.stringify({
109+
provider: "kiln_copilot",
110+
key_data: {
111+
"API Key": apiKey,
112+
},
113+
}),
114+
})
115+
116+
const data = await res.json()
117+
118+
if (!res.ok) {
119+
apiKeyMessage =
120+
data.message || data.detail || "Failed to connect to provider"
121+
apiKeyError = true
122+
return false
123+
}
124+
125+
posthog.capture("connect_provider", {
126+
provider_id: "kiln_copilot",
127+
})
128+
129+
onSuccess()
130+
return true
131+
} catch (e) {
132+
console.error("submitApiKey error", e)
133+
apiKeyMessage = "Failed to connect to provider (Exception: " + e + ")"
134+
apiKeyError = true
135+
return false
136+
} finally {
137+
submitting = false
138+
}
139+
}
140+
141+
onMount(async () => {
142+
if (
143+
window.location.search.includes("code=") ||
144+
window.location.search.includes("state=")
145+
) {
146+
await initKindeClient()
147+
}
148+
})
149+
</script>
150+
151+
{#if showTitle}
152+
<h1 class="text-xl font-medium text-center mb-2">Connect Kiln Copilot</h1>
153+
{/if}
154+
155+
<ol class="mb-2 text-gray-700">
156+
<li class="list-decimal pl-1 mx-8 mb-4">
157+
<button class="link" on:click={openSignup}>Sign Up</button>
158+
to create your Kiln Copilot account.
159+
</li>
160+
<li class="list-decimal pl-1 mx-8 my-4">
161+
After registration,
162+
<button class="link" on:click={openSelfServePortal}>
163+
open the self-serve portal.
164+
</button>
165+
Go to 'API keys' section and create an API key.
166+
</li>
167+
<li class="list-decimal pl-1 mx-8 my-4">
168+
Copy your API key, paste it below and click 'Connect'.
169+
</li>
170+
</ol>
171+
172+
{#if apiKeyMessage}
173+
<p class="text-error text-center pb-4">{apiKeyMessage}</p>
174+
{/if}
175+
176+
<div class="flex flex-row gap-4 items-center">
177+
<div class="grow flex flex-col gap-2">
178+
<input
179+
type="text"
180+
id="API Key"
181+
placeholder="API Key"
182+
class="input input-bordered w-full max-w-[300px] {apiKeyError
183+
? 'input-error'
184+
: ''}"
185+
bind:value={apiKey}
186+
on:input={() => (apiKeyError = false)}
187+
/>
188+
</div>
189+
<button
190+
class="btn min-w-[130px]"
191+
on:click={submitApiKey}
192+
disabled={submitting}
193+
>
194+
{#if submitting}
195+
<div class="loading loading-spinner loading-md"></div>
196+
{:else if showCheckmark}
197+
<img
198+
src="/images/circle-check.svg"
199+
class="size-6 group-hover:hidden"
200+
alt="Connected"
201+
/>
202+
{:else}
203+
Connect
204+
{/if}
205+
</button>
206+
</div>

app/web_ui/src/lib/utils/form_container.svelte

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
export let submit_disabled = false
3939
export let gap: number = 6
4040
export let focus_on_mount = true
41+
export let compact_button = false
4142
$: ui_saved_indicator = update_ui_saved_indicator(saved)
4243
4344
function update_ui_saved_indicator(saved: boolean): boolean {
@@ -173,7 +174,7 @@
173174
<form class="flex flex-col gap-{gap} w-full" {id} on:submit={handleFormSubmit}>
174175
<slot />
175176

176-
<div class="flex flex-col gap-2">
177+
<div class="flex flex-col gap-2 {compact_button ? 'items-end' : ''}">
177178
{#if has_validation_errors}
178179
<div class="text-sm text-center text-error">
179180
<button class="link" on:click={() => focus_first_error()}
@@ -192,7 +193,9 @@
192193
type="submit"
193194
class="relative btn {primary ? 'btn-primary' : ''} {ui_saved_indicator
194195
? 'btn-success'
195-
: ''} {submit_visible ? '' : 'hidden'}"
196+
: ''} {submit_visible ? '' : 'hidden'} {compact_button
197+
? 'min-w-64 px-12'
198+
: 'w-full'}"
196199
on:click={validate_and_submit}
197200
disabled={submitting || submit_disabled}
198201
>

app/web_ui/src/lib/utils/formatters.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import {
22
type ChunkerConfig,
33
type ChunkerType,
4+
type EvalConfig,
45
type EvalConfigType,
56
type OutputFormat,
7+
type ProviderModels,
68
type SpecType,
79
type StructuredOutputMode,
810
type ToolServerType,
911
} from "$lib/types"
12+
import { model_name, provider_name_from_id } from "$lib/stores"
1013
import {
1114
fixedWindowChunkerProperties,
1215
semanticChunkerProperties,
@@ -301,3 +304,25 @@ export function toolServerTypeToString(
301304
}
302305
}
303306
}
307+
308+
/**
309+
* Format an eval config name for display in dropdowns and properties.
310+
* Format: "{name} — {config_type}, {model_name} ({provider})"
311+
* Example: "Electric Dragon — G-eval, GPT 4.1 (OpenAI)"
312+
* If compact is true, it will only return the name and model name.
313+
* Example: "Electric Dragon — GPT 4.1"
314+
*/
315+
export function formatEvalConfigName(
316+
eval_config: EvalConfig,
317+
model_info: ProviderModels | null,
318+
compact: boolean = false,
319+
): string {
320+
const model_name_value = model_name(eval_config.model_name, model_info)
321+
const parts = compact
322+
? [model_name_value]
323+
: [
324+
eval_config_to_ui_name(eval_config.config_type),
325+
`${model_name_value} (${provider_name_from_id(eval_config.model_provider)})`,
326+
]
327+
return eval_config.name + " — " + parts.join(", ")
328+
}

app/web_ui/src/routes/(app)/settings/providers/kiln_copilot/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</script>
66

77
<AppPage
8-
title="Connect Kiln Copilot"
8+
title="Kiln Copilot"
99
breadcrumbs={[
1010
{ label: "Settings", href: "/settings" },
1111
{ label: "AI Providers", href: "/settings/providers" },

0 commit comments

Comments
 (0)