Expose chat-latest and default Quick Question to it
This commit is contained in:
+2
-2
@@ -31,7 +31,7 @@ Chat upload limits:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"providers": {
|
"providers": {
|
||||||
"openai": { "models": ["gpt-4.1-mini"], "loadedAt": "2026-02-14T00:00:00.000Z", "error": null },
|
"openai": { "models": ["chat-latest", "gpt-4.1-mini"], "loadedAt": "2026-02-14T00:00:00.000Z", "error": null },
|
||||||
"anthropic": { "models": ["claude-3-5-sonnet-latest"], "loadedAt": null, "error": null },
|
"anthropic": { "models": ["claude-3-5-sonnet-latest"], "loadedAt": null, "error": null },
|
||||||
"xai": { "models": ["grok-3-mini"], "loadedAt": null, "error": null },
|
"xai": { "models": ["grok-3-mini"], "loadedAt": null, "error": null },
|
||||||
"gemini": { "models": ["gemini-3.5-flash"], "loadedAt": null, "error": null },
|
"gemini": { "models": ["gemini-3.5-flash"], "loadedAt": null, "error": null },
|
||||||
@@ -39,7 +39,7 @@ Chat upload limits:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
- OpenAI model lists are filtered to models that are expected to work with the backend's Responses API implementation.
|
- OpenAI model lists are filtered to models that are expected to work with the backend's Responses API implementation, including the exact `chat-latest` alias when returned by OpenAI. Audio, image-generation, embedding, moderation, and other specialized model ids remain excluded. See [OpenAI's Chat Latest documentation](https://developers.openai.com/api/docs/models/chat-latest) for supported capabilities.
|
||||||
- Gemini model lists are loaded from Google's native Models API and filtered to Gemini `generateContent` model ids.
|
- Gemini model lists are loaded from Google's native Models API and filtered to Gemini `generateContent` model ids.
|
||||||
- `hermes-agent` is included only when `HERMES_AGENT_API_KEY` is configured. Set it to Hermes `API_SERVER_KEY`, or any non-empty value if that local server does not require auth. `HERMES_AGENT_API_BASE_URL` defaults to `http://127.0.0.1:8642/v1`; set `HERMES_AGENT_MODEL` only when you need an additional fallback/override model id.
|
- `hermes-agent` is included only when `HERMES_AGENT_API_KEY` is configured. Set it to Hermes `API_SERVER_KEY`, or any non-empty value if that local server does not require auth. `HERMES_AGENT_API_BASE_URL` defaults to `http://127.0.0.1:8642/v1`; set `HERMES_AGENT_MODEL` only when you need an additional fallback/override model id.
|
||||||
- The backend loads provider model lists at startup and refreshes them about once every 24 hours. If a later provider refresh fails, the response keeps the last loaded model list for that provider and sets `error` to the latest failure message.
|
- The backend loads provider model lists at startup and refreshes them about once every 24 hours. If a later provider refresh fails, the response keeps the last loaded model list for that provider and sets `error` to the latest failure message.
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ Request body:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Behavior notes:
|
Behavior notes:
|
||||||
|
- `provider` and `model` are required. Web and native clients default Quick Question to `provider: "openai"` and `model: "chat-latest"` when no Quick Question selection has been saved. Saved Quick Question choices take precedence over this default and are separate from regular chat preferences.
|
||||||
- `question` is required, trimmed by the server, and must not be empty.
|
- `question` is required, trimmed by the server, and must not be empty.
|
||||||
- The server prepends a Quick Question system prompt that asks for a succinct, direct, self-contained answer without follow-up questions. Clients do not send or maintain this prompt.
|
- The server prepends a Quick Question system prompt that asks for a succinct, direct, self-contained answer without follow-up questions. Clients do not send or maintain this prompt.
|
||||||
- Quick Questions are always non-persistent. The endpoint does not create a chat or store messages, tool-call logs, assistant output, or `LlmCall` metadata.
|
- Quick Questions are always non-persistent. The endpoint does not create a chat or store messages, tool-call logs, assistant output, or `LlmCall` metadata.
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ final class SybilSettingsStore {
|
|||||||
self.preferredModelByProvider = preferredModels
|
self.preferredModelByProvider = preferredModels
|
||||||
|
|
||||||
self.quickQuestionPreferredProvider =
|
self.quickQuestionPreferredProvider =
|
||||||
defaults.string(forKey: Keys.quickQuestionPreferredProvider).flatMap(Provider.init(rawValue:)) ?? provider
|
defaults.string(forKey: Keys.quickQuestionPreferredProvider).flatMap(Provider.init(rawValue:)) ?? .openai
|
||||||
self.quickQuestionPreferredModelByProvider = [
|
self.quickQuestionPreferredModelByProvider = [
|
||||||
.openai: defaults.string(forKey: Keys.quickQuestionPreferredOpenAIModel) ?? preferredModels[.openai] ?? "gpt-4.1-mini",
|
.openai: defaults.string(forKey: Keys.quickQuestionPreferredOpenAIModel) ?? "chat-latest",
|
||||||
.anthropic: defaults.string(forKey: Keys.quickQuestionPreferredAnthropicModel) ?? preferredModels[.anthropic] ?? "claude-3-5-sonnet-latest",
|
.anthropic: defaults.string(forKey: Keys.quickQuestionPreferredAnthropicModel) ?? preferredModels[.anthropic] ?? "claude-3-5-sonnet-latest",
|
||||||
.xai: defaults.string(forKey: Keys.quickQuestionPreferredXAIModel) ?? preferredModels[.xai] ?? "grok-3-mini",
|
.xai: defaults.string(forKey: Keys.quickQuestionPreferredXAIModel) ?? preferredModels[.xai] ?? "grok-3-mini",
|
||||||
.gemini: defaults.string(forKey: Keys.quickQuestionPreferredGeminiModel) ?? preferredModels[.gemini] ?? "gemini-3.5-flash",
|
.gemini: defaults.string(forKey: Keys.quickQuestionPreferredGeminiModel) ?? preferredModels[.gemini] ?? "gemini-3.5-flash",
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ final class SybilViewModel {
|
|||||||
private let clientFactory: (APIConfiguration) -> any SybilAPIClienting
|
private let clientFactory: (APIConfiguration) -> any SybilAPIClienting
|
||||||
|
|
||||||
private let fallbackModels: [Provider: [String]] = [
|
private let fallbackModels: [Provider: [String]] = [
|
||||||
.openai: ["gpt-4.1-mini"],
|
.openai: ["gpt-4.1-mini", "chat-latest"],
|
||||||
.anthropic: ["claude-3-5-sonnet-latest"],
|
.anthropic: ["claude-3-5-sonnet-latest"],
|
||||||
.xai: ["grok-3-mini"],
|
.xai: ["grok-3-mini"],
|
||||||
.gemini: ["gemini-3.5-flash", "gemini-flash-latest"],
|
.gemini: ["gemini-3.5-flash", "gemini-flash-latest"],
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ private actor MockSybilClient: SybilAPIClienting {
|
|||||||
private let updateChatStarResponses: [String: ChatSummary]
|
private let updateChatStarResponses: [String: ChatSummary]
|
||||||
private let updateSearchStarResponses: [String: SearchSummary]
|
private let updateSearchStarResponses: [String: SearchSummary]
|
||||||
private let activeRunsResponse: ActiveRunsResponse
|
private let activeRunsResponse: ActiveRunsResponse
|
||||||
|
private let modelCatalogResponse: ModelCatalogResponse
|
||||||
|
|
||||||
private var snapshot = MockClientCallSnapshot()
|
private var snapshot = MockClientCallSnapshot()
|
||||||
private var lastCreateChatCall: ChatCreateCallSnapshot?
|
private var lastCreateChatCall: ChatCreateCallSnapshot?
|
||||||
@@ -84,7 +85,8 @@ private actor MockSybilClient: SybilAPIClienting {
|
|||||||
updateChatStarResponses: [String: ChatSummary] = [:],
|
updateChatStarResponses: [String: ChatSummary] = [:],
|
||||||
updateSearchStarResponses: [String: SearchSummary] = [:],
|
updateSearchStarResponses: [String: SearchSummary] = [:],
|
||||||
activeRunsResponse: ActiveRunsResponse = ActiveRunsResponse(),
|
activeRunsResponse: ActiveRunsResponse = ActiveRunsResponse(),
|
||||||
workspaceItemsResponse: [WorkspaceItem]? = nil
|
workspaceItemsResponse: [WorkspaceItem]? = nil,
|
||||||
|
modelCatalogResponse: ModelCatalogResponse = ModelCatalogResponse(providers: [:])
|
||||||
) {
|
) {
|
||||||
self.chatsResponse = chatsResponse
|
self.chatsResponse = chatsResponse
|
||||||
self.searchesResponse = searchesResponse
|
self.searchesResponse = searchesResponse
|
||||||
@@ -98,6 +100,7 @@ private actor MockSybilClient: SybilAPIClienting {
|
|||||||
self.updateChatStarResponses = updateChatStarResponses
|
self.updateChatStarResponses = updateChatStarResponses
|
||||||
self.updateSearchStarResponses = updateSearchStarResponses
|
self.updateSearchStarResponses = updateSearchStarResponses
|
||||||
self.activeRunsResponse = activeRunsResponse
|
self.activeRunsResponse = activeRunsResponse
|
||||||
|
self.modelCatalogResponse = modelCatalogResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func makeWorkspaceItems(chats: [ChatSummary], searches: [SearchSummary]) -> [WorkspaceItem] {
|
private static func makeWorkspaceItems(chats: [ChatSummary], searches: [SearchSummary]) -> [WorkspaceItem] {
|
||||||
@@ -306,7 +309,7 @@ private actor MockSybilClient: SybilAPIClienting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listModels() async throws -> ModelCatalogResponse {
|
func listModels() async throws -> ModelCatalogResponse {
|
||||||
ModelCatalogResponse(providers: [:])
|
modelCatalogResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
func getActiveRuns() async throws -> ActiveRunsResponse {
|
func getActiveRuns() async throws -> ActiveRunsResponse {
|
||||||
@@ -1257,6 +1260,7 @@ private func makeCompletionToolCall(
|
|||||||
#expect(snapshot.runQuickQuestionStream == 1)
|
#expect(snapshot.runQuickQuestionStream == 1)
|
||||||
#expect(snapshot.runCompletionStream == 0)
|
#expect(snapshot.runCompletionStream == 0)
|
||||||
#expect(body?.provider == .openai)
|
#expect(body?.provider == .openai)
|
||||||
|
#expect(body?.model == "chat-latest")
|
||||||
#expect(body?.question == "How do I reset my password?")
|
#expect(body?.question == "How do I reset my password?")
|
||||||
#expect(viewModel.quickQuestionAnswerText == "Reset it from Settings.")
|
#expect(viewModel.quickQuestionAnswerText == "Reset it from Settings.")
|
||||||
#expect(!viewModel.isQuickQuestionSending)
|
#expect(!viewModel.isQuickQuestionSending)
|
||||||
@@ -1370,6 +1374,42 @@ private func makeCompletionToolCall(
|
|||||||
#expect(viewModel.quickQuestionPrompt.isEmpty)
|
#expect(viewModel.quickQuestionPrompt.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
@Test func quickQuestionDefaultsToChatLatestIndependentlyOfRegularChatPreferences() async {
|
||||||
|
let defaults = UserDefaults(suiteName: #function)!
|
||||||
|
defaults.removePersistentDomain(forName: #function)
|
||||||
|
defer { defaults.removePersistentDomain(forName: #function) }
|
||||||
|
defaults.set("anthropic", forKey: "sybil.ios.preferredProvider")
|
||||||
|
defaults.set("gpt-4o", forKey: "sybil.ios.preferredOpenAIModel")
|
||||||
|
let settings = SybilSettingsStore(defaults: defaults)
|
||||||
|
let client = MockSybilClient(modelCatalogResponse: ModelCatalogResponse(providers: [
|
||||||
|
.openai: ProviderModelInfo(models: ["gpt-4.1-mini", "chat-latest"], loadedAt: nil, error: nil),
|
||||||
|
.anthropic: ProviderModelInfo(models: ["claude-3-5-sonnet-latest"], loadedAt: nil, error: nil)
|
||||||
|
]))
|
||||||
|
let viewModel = SybilViewModel(settings: settings) { _ in client }
|
||||||
|
|
||||||
|
#expect(viewModel.provider == .anthropic)
|
||||||
|
#expect(settings.preferredModelByProvider[.openai] == "gpt-4o")
|
||||||
|
#expect(viewModel.quickQuestionProvider == .openai)
|
||||||
|
#expect(viewModel.quickQuestionModel == "chat-latest")
|
||||||
|
#expect(viewModel.quickQuestionProviderModelOptions.contains("chat-latest"))
|
||||||
|
|
||||||
|
viewModel.setQuickQuestionProvider(.anthropic)
|
||||||
|
viewModel.setQuickQuestionProvider(.openai)
|
||||||
|
#expect(viewModel.quickQuestionModel == "chat-latest")
|
||||||
|
|
||||||
|
await viewModel.bootstrap()
|
||||||
|
#expect(viewModel.quickQuestionModel == "chat-latest")
|
||||||
|
#expect(viewModel.quickQuestionProviderModelOptions == ["gpt-4.1-mini", "chat-latest"])
|
||||||
|
|
||||||
|
viewModel.setQuickQuestionProvider(.anthropic)
|
||||||
|
viewModel.setQuickQuestionProvider(.openai)
|
||||||
|
#expect(viewModel.quickQuestionModel == "chat-latest")
|
||||||
|
#expect(SybilSettingsStore(defaults: defaults).quickQuestionPreferredModelByProvider[.openai] == "chat-latest")
|
||||||
|
#expect(settings.preferredProvider == .anthropic)
|
||||||
|
#expect(settings.preferredModelByProvider[.openai] == "gpt-4o")
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@Test func quickQuestionProviderAndModelSelectionPersistSeparately() async throws {
|
@Test func quickQuestionProviderAndModelSelectionPersistSeparately() async throws {
|
||||||
let defaults = UserDefaults(suiteName: #function)!
|
let defaults = UserDefaults(suiteName: #function)!
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ second **Option+Space**, the close button, or clicking outside dismisses it.
|
|||||||
Hiding the panel preserves the draft and answer and lets an in-flight answer finish.
|
Hiding the panel preserves the draft and answer and lets an in-flight answer finish.
|
||||||
**Open in Chat** saves the question and answer in the regular workspace.
|
**Open in Chat** saves the question and answer in the regular workspace.
|
||||||
|
|
||||||
|
Quick Question defaults to OpenAI `chat-latest` on iPhone, iPad, and Mac. Its saved
|
||||||
|
provider/model selection takes precedence and is separate from regular chat preferences.
|
||||||
|
|
||||||
The regular Mac chat/search composer uses the same Return/Shift+Return rules.
|
The regular Mac chat/search composer uses the same Return/Shift+Return rules.
|
||||||
Holding Return does not repeatedly submit. On iOS, Return submits from Quick
|
Holding Return does not repeatedly submit. On iOS, Return submits from Quick
|
||||||
Question, while the normal chat/search composer continues to use Return for a
|
Question, while the normal chat/search composer continues to use Return for a
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ function isLikelyResponsesApiModel(model: string) {
|
|||||||
if (id.includes("audio") || id.includes("realtime") || id.includes("transcribe") || id.includes("tts")) return false;
|
if (id.includes("audio") || id.includes("realtime") || id.includes("transcribe") || id.includes("tts")) return false;
|
||||||
if (id.includes("image") || id.includes("dall-e") || id.includes("sora")) return false;
|
if (id.includes("image") || id.includes("dall-e") || id.includes("sora")) return false;
|
||||||
if (id.includes("search") || id.includes("computer-use")) return false;
|
if (id.includes("search") || id.includes("computer-use")) return false;
|
||||||
return /^(gpt-|o\d|chatgpt-)/.test(id);
|
return id === "chat-latest" || /^(gpt-|o\d|chatgpt-)/.test(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLikelyGeminiChatModel(model: string) {
|
function isLikelyGeminiChatModel(model: string) {
|
||||||
|
|||||||
@@ -1,6 +1,50 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import { describeProviderChatBackend } from "../src/llm/provider-adapters.js";
|
import { env } from "../src/env.js";
|
||||||
|
import { describeProviderChatBackend, fetchProviderCatalogModels } from "../src/llm/provider-adapters.js";
|
||||||
|
|
||||||
|
for (const { name, listedModels, expectedModels } of [
|
||||||
|
{
|
||||||
|
name: "OpenAI catalog includes chat-latest while excluding non-chat models",
|
||||||
|
listedModels: [
|
||||||
|
"gpt-4.1-mini",
|
||||||
|
"chat-latest",
|
||||||
|
"o3",
|
||||||
|
"chatgpt-4o-latest",
|
||||||
|
"chat-latest",
|
||||||
|
"text-embedding-3-small",
|
||||||
|
"omni-moderation-latest",
|
||||||
|
"gpt-4o-audio-preview",
|
||||||
|
"gpt-realtime",
|
||||||
|
"gpt-4o-transcribe",
|
||||||
|
"gpt-4o-mini-tts",
|
||||||
|
"gpt-image-1",
|
||||||
|
"chatgpt-image-latest",
|
||||||
|
"gpt-4o-search-preview",
|
||||||
|
"computer-use-preview",
|
||||||
|
"sora-2",
|
||||||
|
],
|
||||||
|
expectedModels: ["chat-latest", "chatgpt-4o-latest", "gpt-4.1-mini", "o3"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OpenAI catalog does not invent models absent from the provider response",
|
||||||
|
listedModels: ["gpt-4.1-mini"],
|
||||||
|
expectedModels: ["gpt-4.1-mini"],
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
test(name, async (t) => {
|
||||||
|
const originalKey = env.OPENAI_API_KEY;
|
||||||
|
env.OPENAI_API_KEY = "test-openai-key";
|
||||||
|
t.after(() => { env.OPENAI_API_KEY = originalKey; });
|
||||||
|
t.mock.method(globalThis, "fetch", async (input: RequestInfo | URL) => {
|
||||||
|
const url = input instanceof Request ? input.url : String(input);
|
||||||
|
assert.equal(url, "https://api.openai.com/v1/models");
|
||||||
|
return Response.json({ data: listedModels.map((id) => ({ id })) });
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(await fetchProviderCatalogModels("openai"), expectedModels);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test("provider backend registry selects chat protocol and managed-tool mode", () => {
|
test("provider backend registry selects chat protocol and managed-tool mode", () => {
|
||||||
assert.deepEqual(describeProviderChatBackend("openai", []), {
|
assert.deepEqual(describeProviderChatBackend("openai", []), {
|
||||||
|
|||||||
+5
-5
@@ -80,7 +80,7 @@ import {
|
|||||||
resolveSidebarSelectionAfterRefresh,
|
resolveSidebarSelectionAfterRefresh,
|
||||||
type SidebarSelection,
|
type SidebarSelection,
|
||||||
} from "@/lib/sidebar-selection";
|
} from "@/lib/sidebar-selection";
|
||||||
import { buildQuickQuestionRequest } from "@/lib/quick-question";
|
import { buildQuickQuestionRequest, pickQuickQuestionModel, QUICK_QUESTION_DEFAULT_MODEL } from "@/lib/quick-question";
|
||||||
import {
|
import {
|
||||||
appendStreamingDelta,
|
appendStreamingDelta,
|
||||||
clearStreamingAssistantTrace,
|
clearStreamingAssistantTrace,
|
||||||
@@ -161,7 +161,7 @@ function buildWorkspaceUrl(selection: SidebarSelection | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PROVIDER_FALLBACK_MODELS: Record<Provider, string[]> = {
|
const PROVIDER_FALLBACK_MODELS: Record<Provider, string[]> = {
|
||||||
openai: ["gpt-4.1-mini"],
|
openai: ["gpt-4.1-mini", QUICK_QUESTION_DEFAULT_MODEL],
|
||||||
anthropic: ["claude-3-5-sonnet-latest"],
|
anthropic: ["claude-3-5-sonnet-latest"],
|
||||||
xai: ["grok-3-mini"],
|
xai: ["grok-3-mini"],
|
||||||
gemini: ["gemini-3.5-flash", "gemini-flash-latest"],
|
gemini: ["gemini-3.5-flash", "gemini-flash-latest"],
|
||||||
@@ -991,7 +991,7 @@ export default function App() {
|
|||||||
);
|
);
|
||||||
const [quickModel, setQuickModel] = useState(() => {
|
const [quickModel, setQuickModel] = useState(() => {
|
||||||
const stored = loadStoredQuickQuestionModelSelection();
|
const stored = loadStoredQuickQuestionModelSelection();
|
||||||
return stored.modelPreferences[stored.provider] ?? PROVIDER_FALLBACK_MODELS[stored.provider][0];
|
return pickQuickQuestionModel(stored.provider, PROVIDER_FALLBACK_MODELS[stored.provider], stored.modelPreferences[stored.provider]);
|
||||||
});
|
});
|
||||||
const [isQuickQuestionOpen, setIsQuickQuestionOpen] = useState(false);
|
const [isQuickQuestionOpen, setIsQuickQuestionOpen] = useState(false);
|
||||||
const [quickPrompt, setQuickPrompt] = useState("");
|
const [quickPrompt, setQuickPrompt] = useState("");
|
||||||
@@ -1449,7 +1449,7 @@ export default function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (quickModel.trim()) return;
|
if (quickModel.trim()) return;
|
||||||
setQuickModel((current) => {
|
setQuickModel((current) => {
|
||||||
return current.trim() || pickProviderModel(quickProviderModelOptions, quickProviderModelPreferences[quickProvider]);
|
return current.trim() || pickQuickQuestionModel(quickProvider, quickProviderModelOptions, quickProviderModelPreferences[quickProvider]);
|
||||||
});
|
});
|
||||||
}, [quickModel, quickProvider, quickProviderModelOptions, quickProviderModelPreferences]);
|
}, [quickModel, quickProvider, quickProviderModelOptions, quickProviderModelPreferences]);
|
||||||
|
|
||||||
@@ -4176,7 +4176,7 @@ export default function App() {
|
|||||||
const nextProvider = event.currentTarget.value as Provider;
|
const nextProvider = event.currentTarget.value as Provider;
|
||||||
setQuickProvider(nextProvider);
|
setQuickProvider(nextProvider);
|
||||||
const options = getModelOptions(modelCatalog, nextProvider);
|
const options = getModelOptions(modelCatalog, nextProvider);
|
||||||
setQuickModel(pickProviderModel(options, quickProviderModelPreferences[nextProvider]));
|
setQuickModel(pickQuickQuestionModel(nextProvider, options, quickProviderModelPreferences[nextProvider]));
|
||||||
}}
|
}}
|
||||||
disabled={isQuickQuestionSending || isConvertingQuickQuestion}
|
disabled={isQuickQuestionSending || isConvertingQuickQuestion}
|
||||||
aria-label="Quick question provider"
|
aria-label="Quick question provider"
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import type { Provider } from "./api";
|
import type { Provider } from "./api";
|
||||||
|
|
||||||
|
export const QUICK_QUESTION_DEFAULT_MODEL = "chat-latest";
|
||||||
|
|
||||||
|
export function pickQuickQuestionModel(provider: Provider, options: string[], preferred: string | null = null) {
|
||||||
|
if (preferred?.trim()) return preferred.trim();
|
||||||
|
if (provider === "openai") return QUICK_QUESTION_DEFAULT_MODEL;
|
||||||
|
return options[0] ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
export function buildQuickQuestionRequest({
|
export function buildQuickQuestionRequest({
|
||||||
provider,
|
provider,
|
||||||
model,
|
model,
|
||||||
|
|||||||
@@ -1,6 +1,27 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import { buildQuickQuestionRequest } from "../src/lib/quick-question.ts";
|
import { buildQuickQuestionRequest, pickQuickQuestionModel } from "../src/lib/quick-question.ts";
|
||||||
|
|
||||||
|
test("quick questions default to chat-latest before and after the model catalog loads", () => {
|
||||||
|
for (const options of [[], ["gpt-4.1-mini"], ["gpt-4.1-mini", "chat-latest"]]) {
|
||||||
|
const model = pickQuickQuestionModel("openai", options);
|
||||||
|
assert.equal(model, "chat-latest");
|
||||||
|
assert.deepEqual(buildQuickQuestionRequest({ provider: "openai", model, content: "What is UTC?" }), {
|
||||||
|
provider: "openai",
|
||||||
|
model: "chat-latest",
|
||||||
|
question: "What is UTC?",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("quick questions preserve saved models and use provider-specific choices when switching providers", () => {
|
||||||
|
assert.equal(pickQuickQuestionModel("openai", ["chat-latest"], " gpt-4.1-mini "), "gpt-4.1-mini");
|
||||||
|
assert.equal(pickQuickQuestionModel("openai", ["gpt-4.1-mini"], " "), "chat-latest");
|
||||||
|
assert.equal(pickQuickQuestionModel("anthropic", ["claude-3-5-sonnet-latest"]), "claude-3-5-sonnet-latest");
|
||||||
|
assert.equal(pickQuickQuestionModel("anthropic", ["claude-3-5-sonnet-latest"], "claude-3-haiku"), "claude-3-haiku");
|
||||||
|
assert.equal(pickQuickQuestionModel("anthropic", []), "");
|
||||||
|
assert.equal(pickQuickQuestionModel("openai", ["gpt-4.1-mini", "chat-latest"]), "chat-latest");
|
||||||
|
});
|
||||||
|
|
||||||
test("quick question requests use the dedicated server endpoint shape", () => {
|
test("quick question requests use the dedicated server endpoint shape", () => {
|
||||||
const request = buildQuickQuestionRequest({
|
const request = buildQuickQuestionRequest({
|
||||||
|
|||||||
Reference in New Issue
Block a user