@aituber-onair/chat 0.56.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -252,6 +252,7 @@ var AITuberOnAirChat = (() => {
252
252
  MODEL_ZAI_GLM_5_3_FLASH: () => MODEL_ZAI_GLM_5_3_FLASH,
253
253
  MistralChatService: () => MistralChatService,
254
254
  MistralChatServiceProvider: () => MistralChatServiceProvider,
255
+ OPENAI_COMPATIBLE_LOCAL_PRESETS: () => OPENAI_COMPATIBLE_LOCAL_PRESETS,
255
256
  OPENROUTER_CREDITS_THRESHOLD: () => OPENROUTER_CREDITS_THRESHOLD,
256
257
  OPENROUTER_DEPRECATED_MODELS: () => OPENROUTER_DEPRECATED_MODELS,
257
258
  OPENROUTER_FREE_DAILY_LIMIT_HIGH_CREDITS: () => OPENROUTER_FREE_DAILY_LIMIT_HIGH_CREDITS,
@@ -262,6 +263,7 @@ var AITuberOnAirChat = (() => {
262
263
  OpenAIChatService: () => OpenAIChatService,
263
264
  OpenAIChatServiceProvider: () => OpenAIChatServiceProvider,
264
265
  OpenAICompatibleChatServiceProvider: () => OpenAICompatibleChatServiceProvider,
266
+ OpenAICompatibleEndpointError: () => OpenAICompatibleEndpointError,
265
267
  OpenRouterChatService: () => OpenRouterChatService,
266
268
  OpenRouterChatServiceProvider: () => OpenRouterChatServiceProvider,
267
269
  PLAMO_API_BASE_URL: () => PLAMO_API_BASE_URL,
@@ -333,6 +335,7 @@ var AITuberOnAirChat = (() => {
333
335
  isZaiReasoningEffortModel: () => isZaiReasoningEffortModel,
334
336
  isZaiToolStreamModel: () => isZaiToolStreamModel,
335
337
  isZaiVisionModel: () => isZaiVisionModel,
338
+ listOpenAICompatibleModels: () => listOpenAICompatibleModels,
336
339
  normalizeClaudeReasoningEffort: () => normalizeClaudeReasoningEffort,
337
340
  normalizeDeepSeekReasoningEffort: () => normalizeDeepSeekReasoningEffort,
338
341
  normalizeGeminiReasoningEffort: () => normalizeGeminiReasoningEffort,
@@ -344,11 +347,13 @@ var AITuberOnAirChat = (() => {
344
347
  parseOpenAICompatibleToolStream: () => parseOpenAICompatibleToolStream,
345
348
  processChatWithOptionalTools: () => processChatWithOptionalTools,
346
349
  refreshOpenRouterFreeModels: () => refreshOpenRouterFreeModels,
350
+ resolveOpenAICompatibleEndpoint: () => resolveOpenAICompatibleEndpoint,
347
351
  resolveVisionModel: () => resolveVisionModel,
348
352
  runOnceText: () => runOnceText,
349
353
  safeJsonParse: () => safeJsonParse,
350
354
  safeParseToolCallInput: () => safeParseToolCallInput,
351
355
  screenplayToText: () => screenplayToText,
356
+ testOpenAICompatibleConnection: () => testOpenAICompatibleConnection,
352
357
  textToScreenplay: () => textToScreenplay,
353
358
  textsToScreenplay: () => textsToScreenplay
354
359
  });
@@ -1826,6 +1831,192 @@ If it's in another language, summarize in that language.
1826
1831
  };
1827
1832
  }
1828
1833
 
1834
+ // src/utils/openaiCompatibleEndpoint.ts
1835
+ var OPENAI_COMPATIBLE_LOCAL_PRESETS = [
1836
+ {
1837
+ id: "ollama",
1838
+ label: "Ollama",
1839
+ baseUrl: "http://localhost:11434/v1",
1840
+ docsUrl: "https://docs.ollama.com/api/openai-compatibility",
1841
+ corsHint: "Set OLLAMA_ORIGINS to allow additional browser origins."
1842
+ },
1843
+ {
1844
+ id: "lmStudio",
1845
+ label: "LM Studio",
1846
+ baseUrl: "http://localhost:1234/v1",
1847
+ docsUrl: "https://lmstudio.ai/docs/developer/openai-compat",
1848
+ corsHint: "Start the server with lms server start --cors for browser access."
1849
+ },
1850
+ {
1851
+ id: "llamaCpp",
1852
+ label: "llama.cpp",
1853
+ baseUrl: "http://127.0.0.1:8080/v1",
1854
+ docsUrl: "https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md",
1855
+ corsHint: "All origins are allowed by default; use --cors-origins to restrict them."
1856
+ },
1857
+ {
1858
+ id: "vllm",
1859
+ label: "vLLM",
1860
+ baseUrl: "http://localhost:8000/v1",
1861
+ docsUrl: "https://docs.vllm.ai/en/latest/serving/online_serving/openai_compatible_server/",
1862
+ corsHint: "All origins are allowed by default; use --allowed-origins to restrict them."
1863
+ }
1864
+ ];
1865
+ var OpenAICompatibleEndpointError = class extends Error {
1866
+ constructor(code, message, status) {
1867
+ super(message);
1868
+ this.name = "OpenAICompatibleEndpointError";
1869
+ this.code = code;
1870
+ this.status = status;
1871
+ }
1872
+ };
1873
+ function resolveOpenAICompatibleEndpoint(input) {
1874
+ let url;
1875
+ try {
1876
+ url = new URL(input.trim());
1877
+ } catch {
1878
+ throw new OpenAICompatibleEndpointError(
1879
+ "invalid-url",
1880
+ "Enter a valid HTTP or HTTPS endpoint URL."
1881
+ );
1882
+ }
1883
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.search || url.hash || url.username || url.password) {
1884
+ throw new OpenAICompatibleEndpointError(
1885
+ "invalid-url",
1886
+ "Use an HTTP or HTTPS endpoint URL without credentials, a query, or a fragment."
1887
+ );
1888
+ }
1889
+ const path = url.pathname.replace(/\/+$/, "");
1890
+ if (path.endsWith("/api/chat") || path.endsWith("/api/generate")) {
1891
+ throw new OpenAICompatibleEndpointError(
1892
+ "invalid-url",
1893
+ "This looks like Ollama's native API URL. Use the OpenAI-compatible base URL instead, e.g. http://localhost:11434/v1."
1894
+ );
1895
+ }
1896
+ const basePath = !path || path === "/" ? "/v1" : path.endsWith("/chat/completions") ? path.slice(0, -"/chat/completions".length) : path;
1897
+ const baseUrl = `${url.origin}${basePath}`;
1898
+ return {
1899
+ baseUrl,
1900
+ chatCompletionsUrl: `${baseUrl}/chat/completions`,
1901
+ modelsUrl: `${baseUrl}/models`
1902
+ };
1903
+ }
1904
+ async function listOpenAICompatibleModels(options) {
1905
+ const { modelsUrl } = resolveOpenAICompatibleEndpoint(options.endpoint);
1906
+ const timeoutMs = options.timeoutMs ?? 8e3;
1907
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
1908
+ throw new RangeError("timeoutMs must be a positive finite number.");
1909
+ }
1910
+ if (options.signal?.aborted) {
1911
+ throw new OpenAICompatibleEndpointError(
1912
+ "aborted",
1913
+ "The models request was aborted by the caller."
1914
+ );
1915
+ }
1916
+ const controller = new AbortController();
1917
+ let abortCause;
1918
+ const onAbort = () => {
1919
+ if (!abortCause) abortCause = "caller";
1920
+ controller.abort();
1921
+ };
1922
+ options.signal?.addEventListener("abort", onAbort, { once: true });
1923
+ if (options.signal?.aborted) onAbort();
1924
+ const timeout = setTimeout(() => {
1925
+ if (!abortCause) abortCause = "timeout";
1926
+ controller.abort();
1927
+ }, timeoutMs);
1928
+ try {
1929
+ const headers = {};
1930
+ if (options.apiKey?.trim()) {
1931
+ headers.Authorization = `Bearer ${options.apiKey.trim()}`;
1932
+ }
1933
+ const response = await (options.fetch ?? fetch)(modelsUrl, {
1934
+ method: "GET",
1935
+ headers,
1936
+ signal: controller.signal
1937
+ });
1938
+ if (abortCause) {
1939
+ throw new OpenAICompatibleEndpointError(
1940
+ "aborted",
1941
+ "The models request was aborted by the caller."
1942
+ );
1943
+ }
1944
+ if (!response.ok) {
1945
+ throw new OpenAICompatibleEndpointError(
1946
+ "http",
1947
+ `Model listing failed with HTTP ${response.status}.`,
1948
+ response.status
1949
+ );
1950
+ }
1951
+ let body;
1952
+ try {
1953
+ body = await response.json();
1954
+ } catch {
1955
+ throw new OpenAICompatibleEndpointError(
1956
+ "invalid-response",
1957
+ "The models endpoint did not return valid JSON."
1958
+ );
1959
+ }
1960
+ if (!body || typeof body !== "object" || !("data" in body) || !Array.isArray(body.data) || !body.data.every(
1961
+ (model) => model && typeof model === "object" && "id" in model && typeof model.id === "string"
1962
+ )) {
1963
+ throw new OpenAICompatibleEndpointError(
1964
+ "invalid-response",
1965
+ "The models response must contain data with model IDs."
1966
+ );
1967
+ }
1968
+ if (abortCause) {
1969
+ throw new OpenAICompatibleEndpointError(
1970
+ "aborted",
1971
+ "The models request was aborted by the caller."
1972
+ );
1973
+ }
1974
+ return [...new Set(body.data.map((model) => model.id))];
1975
+ } catch (error) {
1976
+ if (abortCause === "caller") {
1977
+ throw new OpenAICompatibleEndpointError(
1978
+ "aborted",
1979
+ "The models request was aborted by the caller."
1980
+ );
1981
+ }
1982
+ if (abortCause === "timeout") {
1983
+ throw new OpenAICompatibleEndpointError(
1984
+ "timeout",
1985
+ `The models request timed out after ${timeoutMs} ms.`
1986
+ );
1987
+ }
1988
+ if (error instanceof OpenAICompatibleEndpointError) throw error;
1989
+ throw new OpenAICompatibleEndpointError(
1990
+ "network",
1991
+ "Could not reach the models endpoint. In a browser, this is usually a CORS restriction or the server is not running."
1992
+ );
1993
+ } finally {
1994
+ clearTimeout(timeout);
1995
+ options.signal?.removeEventListener("abort", onAbort);
1996
+ }
1997
+ }
1998
+ async function testOpenAICompatibleConnection(options) {
1999
+ const startedAt = Date.now();
2000
+ try {
2001
+ const models = await listOpenAICompatibleModels(options);
2002
+ return {
2003
+ ok: true,
2004
+ latencyMs: Date.now() - startedAt,
2005
+ models,
2006
+ ...options.model !== void 0 ? { modelFound: models.includes(options.model) } : {}
2007
+ };
2008
+ } catch (error) {
2009
+ if (error instanceof RangeError) throw error;
2010
+ return {
2011
+ ok: false,
2012
+ error: error instanceof OpenAICompatibleEndpointError ? error : new OpenAICompatibleEndpointError(
2013
+ "network",
2014
+ "Could not reach the models endpoint. In a browser, this is usually a CORS restriction or the server is not running."
2015
+ )
2016
+ };
2017
+ }
2018
+ }
2019
+
1829
2020
  // src/utils/openaiCompatibleTools.ts
1830
2021
  var buildOpenAICompatibleTools = (tools, format = "chat-completions") => {
1831
2022
  if (tools.length === 0) return [];