@7n/tauri-components 0.5.0 → 0.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/tauri-components",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Shared LLM agent engine + Vue/Quasar UI for Tauri apps (chat, journal, trust-tier approval).",
6
6
  "license": "MIT",
@@ -52,7 +52,7 @@
52
52
  type="textarea"
53
53
  autogrow
54
54
  :label="inputLabel"
55
- hint="наприклад: Create a task named deploy in /Users/.../mt, agent mode"
55
+ :hint="promptHint"
56
56
  />
57
57
 
58
58
  <!-- jscpd:ignore-start — formal footer markup; jscpd html-mode aligns its
@@ -83,6 +83,7 @@ import RequestView from './RequestView.vue'
83
83
  const props = defineProps({
84
84
  modelValue: { type: Boolean, default: false },
85
85
  agent: { type: Object, required: true },
86
+ promptHint: { type: String, default: 'наприклад: Create a task named deploy in /Users/.../mt, agent mode' },
86
87
  })
87
88
  const emit = defineEmits(['update:modelValue', 'ran'])
88
89
 
@@ -0,0 +1,84 @@
1
+ // Resolve the effective omlx base URL: when the myllm reverse proxy
2
+ // (a Tauri app that forwards /v1/* to the real omlx server and logs every
3
+ // request/response for its history view) is alive on its well-known local
4
+ // port, route traffic through it; otherwise talk to omlx directly. The probe
5
+ // hits {proxy origin}/health — the proxy forwards it to omlx itself, so a
6
+ // 2xx proves BOTH the proxy and the upstream are up (omlx down behind a live
7
+ // proxy yields 502 → direct). Framework-free so headless consumers (MCP
8
+ // wrappers, node scripts) can reuse it; pass a Tauri fetch as fetchFn from
9
+ // webview contexts to avoid CORS on localhost.
10
+
11
+ export const DIRECT_OMLX_BASE_URL = 'http://127.0.0.1:8000/v1'
12
+ export const PROXY_OMLX_BASE_URL = 'http://127.0.0.1:8088/v1'
13
+
14
+ const DEFAULT_TIMEOUT_MS = 400
15
+ const DEFAULT_TTL_MS = 12_000
16
+
17
+ /**
18
+ * Is this URL the default local omlx server — the only target eligible for
19
+ * the proxy override? A user who deliberately pointed an app at another
20
+ * host/port must never be silently rerouted (the proxy's upstream is
21
+ * hardwired to the local :8000).
22
+ * @param {string} url candidate base URL
23
+ * @returns {boolean} true for 127.0.0.1:8000 / localhost:8000, false otherwise (incl. parse errors)
24
+ */
25
+ export function isDirectOmlxUrl(url) {
26
+ try {
27
+ const { host } = new URL(url)
28
+ return host === '127.0.0.1:8000' || host === 'localhost:8000'
29
+ }
30
+ catch {
31
+ return false
32
+ }
33
+ }
34
+
35
+ /**
36
+ * One-shot probe: GET {proxy origin}/health with a short timeout. 2xx means
37
+ * the proxy (and omlx behind it) is alive → use the proxy; anything else
38
+ * (timeout, refused, 502) → use the direct URL.
39
+ * @param {{ directUrl?: string, proxyUrl?: string, fetchFn?: typeof fetch, timeoutMs?: number }} [params] config
40
+ * @returns {Promise<string>} the base URL to use
41
+ */
42
+ export async function resolveOmlxBaseUrl({
43
+ directUrl = DIRECT_OMLX_BASE_URL,
44
+ proxyUrl = PROXY_OMLX_BASE_URL,
45
+ fetchFn = fetch,
46
+ timeoutMs = DEFAULT_TIMEOUT_MS,
47
+ } = {}) {
48
+ try {
49
+ const healthUrl = new URL('/health', proxyUrl).toString()
50
+ const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(timeoutMs) : undefined
51
+ const response = await fetchFn(healthUrl, { signal })
52
+ return response.ok ? proxyUrl : directUrl
53
+ }
54
+ catch {
55
+ return directUrl
56
+ }
57
+ }
58
+
59
+ // proxyUrl → { promise, expiresAt }. Caching the promise (not the value)
60
+ // dedupes concurrent probes: several composables firing loadEnv() at once
61
+ // trigger a single /health request.
62
+ const cache = new Map()
63
+
64
+ /**
65
+ * Cached {@link resolveOmlxBaseUrl}: at most one probe per proxyUrl per TTL
66
+ * window, so callers may resolve before every LLM call without paying the
67
+ * probe latency each time, while still noticing the proxy starting/stopping
68
+ * within one TTL.
69
+ * @param {{ directUrl?: string, proxyUrl?: string, fetchFn?: typeof fetch, timeoutMs?: number, ttlMs?: number, now?: () => number }} [params] config
70
+ * @returns {Promise<string>} the base URL to use
71
+ */
72
+ export function resolveOmlxBaseUrlCached({ ttlMs = DEFAULT_TTL_MS, now = Date.now, ...probeOptions } = {}) {
73
+ const proxyUrl = probeOptions.proxyUrl ?? PROXY_OMLX_BASE_URL
74
+ const cached = cache.get(proxyUrl)
75
+ if (cached && now() < cached.expiresAt) return cached.promise
76
+ const promise = resolveOmlxBaseUrl(probeOptions)
77
+ cache.set(proxyUrl, { promise, expiresAt: now() + ttlMs })
78
+ return promise
79
+ }
80
+
81
+ /** Test hook: drop all cached probe results. */
82
+ export function __resetOmlxBaseUrlCache() {
83
+ cache.clear()
84
+ }
package/src/index.js CHANGED
@@ -8,6 +8,7 @@ export { handleApprove, handleRequest, handleRespond } from './core/agent-handle
8
8
  export { createDispatch, validateInput } from './core/dispatch.js'
9
9
  export { createOpenAiChat, runAgent } from './core/llm.js'
10
10
  export { listOmlxModels, resolveModel } from './core/omlx-models.js'
11
+ export { DIRECT_OMLX_BASE_URL, isDirectOmlxUrl, PROXY_OMLX_BASE_URL, resolveOmlxBaseUrl, resolveOmlxBaseUrlCached } from './core/resolve-omlx-base-url.js'
11
12
  export { listTools, toJsonSchema, toolManifest } from './core/manifest.js'
12
13
  export { classify, DEFAULT_ACTOR_TIERS, scopedManifest, scopedToolNames } from './core/scope.js'
13
14
  export { getTool } from './core/tools.js'
@@ -1,5 +1,7 @@
1
1
  import { ref } from 'vue'
2
2
  import { invoke } from '@tauri-apps/api/core'
3
+ import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
4
+ import { isDirectOmlxUrl, resolveOmlxBaseUrlCached } from '../core/resolve-omlx-base-url.js'
3
5
 
4
6
  // Persisted config for the local omlx server (OpenAI-compatible MLX) that drives
5
7
  // the in-app agent. The API key / base URL come from Rust (omlx_config), which
@@ -9,6 +11,12 @@ import { invoke } from '@tauri-apps/api/core'
9
11
  // when localStorage is empty. Priority for base: localStorage > omlx_config >
10
12
  // hardcoded default. The key always comes from omlx_config.
11
13
  //
14
+ // On top of that, when the resolved base is the default local :8000, loadEnv()
15
+ // probes the myllm reverse proxy (:8088/health, cached with a short TTL) and
16
+ // routes through it while it is alive — a runtime-only override that is never
17
+ // persisted, so the dialog keeps showing/editing the direct URL and a custom
18
+ // (non-:8000) base is never silently rerouted.
19
+ //
12
20
  // storagePrefix namespaces the localStorage keys per app (so `task` and `mlmail`
13
21
  // keep independent base/model). defaults seed first launch.
14
22
 
@@ -70,10 +78,15 @@ export function useOmlx({ storagePrefix = 'agent', defaultBaseUrl = DEFAULT_BASE
70
78
  catch {
71
79
  return // not running under Tauri — keep localStorage / defaults
72
80
  }
73
- if (!env) return
74
- if (env.apiKey) apiKey.value = env.apiKey
75
- if (env.baseUrl && !readStored(baseUrlKey)) baseUrl.value = env.baseUrl
76
- if (env.model && !readStored(modelKey)) model.value = env.model
81
+ if (env) {
82
+ if (env.apiKey) apiKey.value = env.apiKey
83
+ if (env.baseUrl && !readStored(baseUrlKey)) baseUrl.value = env.baseUrl
84
+ if (env.model && !readStored(modelKey)) model.value = env.model
85
+ }
86
+ // myllm proxy override — only for the default local target, runtime-only.
87
+ if (isDirectOmlxUrl(baseUrl.value)) {
88
+ baseUrl.value = await resolveOmlxBaseUrlCached({ directUrl: baseUrl.value, fetchFn: tauriFetch })
89
+ }
77
90
  }
78
91
 
79
92
  /**
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Is this URL the default local omlx server — the only target eligible for
3
+ * the proxy override? A user who deliberately pointed an app at another
4
+ * host/port must never be silently rerouted (the proxy's upstream is
5
+ * hardwired to the local :8000).
6
+ * @param {string} url candidate base URL
7
+ * @returns {boolean} true for 127.0.0.1:8000 / localhost:8000, false otherwise (incl. parse errors)
8
+ */
9
+ export function isDirectOmlxUrl(url: string): boolean;
10
+ /**
11
+ * One-shot probe: GET {proxy origin}/health with a short timeout. 2xx means
12
+ * the proxy (and omlx behind it) is alive → use the proxy; anything else
13
+ * (timeout, refused, 502) → use the direct URL.
14
+ * @param {{ directUrl?: string, proxyUrl?: string, fetchFn?: typeof fetch, timeoutMs?: number }} [params] config
15
+ * @returns {Promise<string>} the base URL to use
16
+ */
17
+ export function resolveOmlxBaseUrl({ directUrl, proxyUrl, fetchFn, timeoutMs, }?: {
18
+ directUrl?: string;
19
+ proxyUrl?: string;
20
+ fetchFn?: typeof fetch;
21
+ timeoutMs?: number;
22
+ }): Promise<string>;
23
+ /**
24
+ * Cached {@link resolveOmlxBaseUrl}: at most one probe per proxyUrl per TTL
25
+ * window, so callers may resolve before every LLM call without paying the
26
+ * probe latency each time, while still noticing the proxy starting/stopping
27
+ * within one TTL.
28
+ * @param {{ directUrl?: string, proxyUrl?: string, fetchFn?: typeof fetch, timeoutMs?: number, ttlMs?: number, now?: () => number }} [params] config
29
+ * @returns {Promise<string>} the base URL to use
30
+ */
31
+ export function resolveOmlxBaseUrlCached({ ttlMs, now, ...probeOptions }?: {
32
+ directUrl?: string;
33
+ proxyUrl?: string;
34
+ fetchFn?: typeof fetch;
35
+ timeoutMs?: number;
36
+ ttlMs?: number;
37
+ now?: () => number;
38
+ }): Promise<string>;
39
+ /** Test hook: drop all cached probe results. */
40
+ export function __resetOmlxBaseUrlCache(): void;
41
+ export const DIRECT_OMLX_BASE_URL: "http://127.0.0.1:8000/v1";
42
+ export const PROXY_OMLX_BASE_URL: "http://127.0.0.1:8088/v1";
package/types/index.d.ts CHANGED
@@ -4,5 +4,6 @@ export { handleApprove, handleRequest, handleRespond } from "./core/agent-handle
4
4
  export { createDispatch, validateInput } from "./core/dispatch.js";
5
5
  export { createOpenAiChat, runAgent } from "./core/llm.js";
6
6
  export { listOmlxModels, resolveModel } from "./core/omlx-models.js";
7
+ export { DIRECT_OMLX_BASE_URL, isDirectOmlxUrl, PROXY_OMLX_BASE_URL, resolveOmlxBaseUrl, resolveOmlxBaseUrlCached } from "./core/resolve-omlx-base-url.js";
7
8
  export { listTools, toJsonSchema, toolManifest } from "./core/manifest.js";
8
9
  export { classify, DEFAULT_ACTOR_TIERS, scopedManifest, scopedToolNames } from "./core/scope.js";