Skip to content

Commit 78225ce

Browse files
feat: task-aware model routing via x-char-task header (#4145)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f5a23ed commit 78225ce

File tree

30 files changed

+502
-123
lines changed

30 files changed

+502
-123
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/api/openapi.gen.json

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,49 @@
172172
}
173173
}
174174
},
175+
"/llm/chat/completions": {
176+
"post": {
177+
"tags": [
178+
"llm"
179+
],
180+
"operationId": "llm_chat_completions",
181+
"parameters": [
182+
{
183+
"name": "x-char-task",
184+
"in": "header",
185+
"description": "Task type for model selection",
186+
"required": false,
187+
"schema": {
188+
"oneOf": [
189+
{
190+
"type": "null"
191+
},
192+
{
193+
"$ref": "#/components/schemas/CharTask"
194+
}
195+
]
196+
}
197+
}
198+
],
199+
"responses": {
200+
"200": {
201+
"description": "Chat completion response (streaming or non-streaming)"
202+
},
203+
"401": {
204+
"description": "Unauthorized"
205+
},
206+
"429": {
207+
"description": "Rate limit exceeded"
208+
},
209+
"502": {
210+
"description": "Upstream provider failed"
211+
},
212+
"504": {
213+
"description": "Request timeout"
214+
}
215+
}
216+
}
217+
},
175218
"/nango/connect-session": {
176219
"post": {
177220
"tags": [
@@ -881,6 +924,14 @@
881924
}
882925
}
883926
},
927+
"CharTask": {
928+
"type": "string",
929+
"enum": [
930+
"chat",
931+
"enhance",
932+
"title"
933+
]
934+
},
884935
"ConnectSessionResponse": {
885936
"type": "object",
886937
"required": [
@@ -1759,12 +1810,6 @@
17591810
"scheme": "bearer",
17601811
"bearerFormat": "JWT",
17611812
"description": "Supabase JWT token"
1762-
},
1763-
"device_fingerprint": {
1764-
"type": "apiKey",
1765-
"in": "header",
1766-
"name": "x-device-fingerprint",
1767-
"description": "Optional device fingerprint for analytics"
17681813
}
17691814
}
17701815
},

apps/api/src/openapi.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
use std::collections::BTreeMap;
22

33
use utoipa::openapi::path::{Operation, PathItem};
4-
use utoipa::openapi::security::{
5-
ApiKey, ApiKeyValue, Http, HttpAuthScheme, SecurityRequirement, SecurityScheme,
6-
};
4+
use utoipa::openapi::security::{Http, HttpAuthScheme, SecurityRequirement, SecurityScheme};
75
use utoipa::{Modify, OpenApi};
86

97
#[derive(OpenApi)]
@@ -71,13 +69,6 @@ impl Modify for SecurityAddon {
7169
.build(),
7270
),
7371
);
74-
components.add_security_scheme(
75-
"device_fingerprint",
76-
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::with_description(
77-
"x-device-fingerprint",
78-
"Optional device fingerprint for analytics",
79-
))),
80-
);
8172
}
8273
}
8374
}

apps/desktop/src/components/chat/session.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ function useTransport(
213213
systemPromptOverride?: string,
214214
) {
215215
const registry = useToolRegistry();
216-
const configuredModel = useLanguageModel();
216+
const configuredModel = useLanguageModel("chat");
217217
const model = modelOverride ?? configuredModel;
218218
const language = main.UI.useValue("ai_language", main.STORE_ID) ?? "en";
219219
const [systemPrompt, setSystemPrompt] = useState<string | undefined>();

apps/desktop/src/components/chat/view.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function ChatView() {
1818
currentTab?.type === "sessions" ? currentTab.id : undefined;
1919

2020
const stableSessionId = useStableSessionId(groupId);
21-
const model = useLanguageModel();
21+
const model = useLanguageModel("chat");
2222

2323
const { handleSendMessage } = useChatActions({
2424
groupId,

apps/desktop/src/components/main/body/sessions/floating/options-menu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function OptionsMenu({
6262
const { user_id } = main.UI.useValues(main.STORE_ID);
6363
const updateSessionTabState = useTabs((state) => state.updateSessionTabState);
6464
const createEnhancedNote = useCreateEnhancedNote();
65-
const model = useLanguageModel();
65+
const model = useLanguageModel("enhance");
6666
const generate = useAITask((state) => state.generate);
6767
const selectedTemplateId = settings.UI.useValue(
6868
"selected_template_id",

apps/desktop/src/components/main/body/sessions/note-input/enhanced/enhance-error.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function EnhanceError({
1616
enhancedNoteId: string;
1717
error: Error | undefined;
1818
}) {
19-
const model = useLanguageModel();
19+
const model = useLanguageModel("enhance");
2020
const generate = useAITask((state) => state.generate);
2121
const templateId =
2222
(main.UI.useCell(

apps/desktop/src/components/main/body/sessions/note-input/header.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ function CreateOtherFormatButton({
301301
main.STORE_ID,
302302
);
303303
const createEnhancedNote = useCreateEnhancedNote();
304-
const model = useLanguageModel();
304+
const model = useLanguageModel("enhance");
305305
const openNew = useTabs((state) => state.openNew);
306306

307307
const store = main.UI.useStore(main.STORE_ID);
@@ -611,7 +611,7 @@ function labelForEditorView(view: EditorView): string {
611611
}
612612

613613
function useEnhanceLogic(sessionId: string, enhancedNoteId: string) {
614-
const model = useLanguageModel();
614+
const model = useLanguageModel("enhance");
615615
const llmStatus = useLLMConnectionStatus();
616616
const taskId = createTaskId(enhancedNoteId, "enhance");
617617
const [missingModelError, setMissingModelError] = useState<Error | null>(

apps/desktop/src/hooks/autoEnhance/runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function useAutoEnhanceRunner(
3030
isEnhancing: boolean;
3131
} {
3232
const sessionId = tab.id;
33-
const model = useLanguageModel();
33+
const model = useLanguageModel("enhance");
3434
const { conn: llmConn } = useLLMConnection();
3535
const { updateSessionTabState } = useTabs();
3636
const createEnhancedNote = useCreateEnhancedNote();

apps/desktop/src/hooks/useLLMConnection.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
77
import { extractReasoningMiddleware, wrapLanguageModel } from "ai";
88
import { useMemo } from "react";
99

10+
import type { CharTask } from "@hypr/api-client";
1011
import type { AIProviderStorage } from "@hypr/store";
1112

1213
import { useAuth } from "../auth";
@@ -22,7 +23,7 @@ import {
2223
} from "../components/settings/ai/shared/eligibility";
2324
import { env } from "../env";
2425
import * as settings from "../store/tinybase/store/settings";
25-
import { tracedFetch } from "../utils/traced-fetch";
26+
import { createTracedFetch, tracedFetch } from "../utils/traced-fetch";
2627

2728
type LanguageModelV3 = Parameters<typeof wrapLanguageModel>[0]["model"];
2829

@@ -52,9 +53,12 @@ type LLMConnectionResult = {
5253
status: LLMConnectionStatus;
5354
};
5455

55-
export const useLanguageModel = (): LanguageModelV3 | null => {
56+
export const useLanguageModel = (task?: CharTask): LanguageModelV3 | null => {
5657
const { conn } = useLLMConnection();
57-
return useMemo(() => (conn ? createLanguageModel(conn) : null), [conn]);
58+
return useMemo(
59+
() => (conn ? createLanguageModel(conn, task) : null),
60+
[conn, task],
61+
);
5862
};
5963

6064
export const useLLMConnection = (): LLMConnectionResult => {
@@ -227,11 +231,14 @@ const wrapWithThinkingMiddleware = (
227231
});
228232
};
229233

230-
const createLanguageModel = (conn: LLMConnectionInfo): LanguageModelV3 => {
234+
const createLanguageModel = (
235+
conn: LLMConnectionInfo,
236+
task?: CharTask,
237+
): LanguageModelV3 => {
231238
switch (conn.providerId) {
232239
case "hyprnote": {
233240
const provider = createOpenRouter({
234-
fetch: tracedFetch,
241+
fetch: task ? createTracedFetch(task) : tracedFetch,
235242
baseURL: conn.baseUrl,
236243
apiKey: conn.apiKey,
237244
});

0 commit comments

Comments
 (0)