Author SHA1 Message Date
yilsem 237d1e2060 Refresh AITURK model discovery automatically in desktop beta 4 2026-09-06 01:37:27 +03:00
8 changed files with 111 additions and 10 deletions
+4 -1
View File
@@ -2,7 +2,7 @@
TurkServis'in Hermes Agent tabanlı masaüstü geliştirme ortamı.
- Ürün sürümü: **1.0.0-beta.1**, ilk paket hedefi Windows x64.
- Ürün sürümü: **1.0.0-beta.4**, ilk paket hedefi Windows x64.
- Varsayılan dil Türkçe; ayarlardan diğer dillere geçilebilir.
- İlk kurulum, sohbet, dosyalar, terminal ve temel ayarlar Türkçeleştirilmiştir.
Henüz çevrilmeyen gelişmiş ekranlar ve üçüncü taraf eklentiler İngilizceye döner.
@@ -10,6 +10,9 @@ TurkServis'in Hermes Agent tabanlı masaüstü geliştirme ortamı.
- TurkServis bağlantısı `https://ai.turkservis.online/v1` üzerinden kullanıcının
kendi API anahtarını doğrular ve hesabına açık modelleri dinamik olarak getirir.
Kaynakta veya kurulum paketinde müşteri API anahtarı bulunmaz.
- AITURK model kataloğu arka planda otomatik yenilenir. Açık model listeleri
dakikada bir sunucuyu kontrol eder; 9Router ve OmniRoute'ta yeni yayımlanan
kayıtlar elle liste düzenlemeden görünür. Admin engeli sunucuda uygulanır.
- Bu bağlantı model sağlayıcısıdır; yerel ajan ve dosya araçları bilgisayarda
çalışır. Uzak Hermes gateway bağlantısı farklı bir özelliktir.
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.3",
"version": "1.0.0-beta.4",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis",
"repository": {
@@ -103,7 +103,7 @@ describe('the catalog owns model curation', () => {
renderMenu()
await screen.findByText(/Gemini 2\.5 Flash/i)
const input = screen.getByRole('textbox', { name: 'Search models' })
const input = screen.getByRole('textbox', { name: /Search models|Model ara/ })
fireEvent.change(input, { target: { value: 'gemini-3.1' } })
@@ -116,7 +116,7 @@ describe('the catalog owns model curation', () => {
renderMenu()
await screen.findByText(/Gemini 3\.1 Pro/i)
fireEvent.click(screen.getByText('Edit models…'))
fireEvent.click(screen.getByText(/Edit models…|Modelleri düzenle…/))
expect($modelVisibilityOpen.get()).toBe(true)
})
+25 -1
View File
@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { QueryObserver, focusManager } from '@tanstack/react-query'
import { invalidateProfileScopedQueries, queryClient } from './query-client'
@@ -11,6 +12,29 @@ describe('invalidateProfileScopedQueries', () => {
queryClient.clear()
})
it('refreshes an observed model list without reopening the picker', async () => {
vi.useFakeTimers()
focusManager.setFocused(true)
let models = ['old-model']
const observer = new QueryObserver(queryClient, {
queryKey: ['model-options', 'selected-profile'],
queryFn: async () => models
})
const unsubscribe = observer.subscribe(() => {})
try {
await observer.refetch()
expect(observer.getCurrentResult().data).toEqual(['old-model'])
models = ['new-model', 'new-combo']
await vi.advanceTimersByTimeAsync(60_000)
expect(observer.getCurrentResult().data).toEqual(models)
} finally {
unsubscribe()
queryClient.clear()
focusManager.setFocused(undefined)
vi.useRealTimers()
}
})
it('invalidates profile-scoped caches and leaves account/global caches intact', () => {
const profileScoped = [
['hermes-config-record'],
+6
View File
@@ -12,6 +12,12 @@ export const queryClient = new QueryClient({
}
})
queryClient.setQueryDefaults(['model-options'], {
staleTime: 30_000,
refetchInterval: 60_000,
refetchOnWindowFocus: true
})
// Curried, setState-shaped cache writer for optimistic write-through: keeps
// mutation sites terse (`setX(next)` or `setX(prev => …)`) over one query key.
export const writeCache =
+17 -4
View File
@@ -8,6 +8,7 @@ Add, remove, or reorder entries here — both `hermes setup` and
from __future__ import annotations
import copy
import contextvars
import json
import http.client
import logging
@@ -4809,10 +4810,11 @@ def _spawn_swr_refresh(cache_key: str, refresh_fn=None) -> None:
``PROVIDER_REGISTRY`` slug and refreshed via :func:`provider_model_ids`
(the original behavior).
"""
refresh_identity = (str(_provider_models_cache_path()), cache_key)
with _swr_refresh_lock:
if cache_key in _swr_refresh_inflight:
if refresh_identity in _swr_refresh_inflight:
return
_swr_refresh_inflight.add(cache_key)
_swr_refresh_inflight.add(refresh_identity)
def _default_refresh():
live = provider_model_ids(cache_key, force_refresh=True)
@@ -4847,10 +4849,10 @@ def _spawn_swr_refresh(cache_key: str, refresh_fn=None) -> None:
logger.debug("SWR refresh failed for %s", cache_key, exc_info=True)
finally:
with _swr_refresh_lock:
_swr_refresh_inflight.discard(cache_key)
_swr_refresh_inflight.discard(refresh_identity)
threading.Thread(
target=_refresh, daemon=True, name=f"model-cache-swr-{cache_key}"
target=contextvars.copy_context().run, args=(_refresh,), daemon=True, name=f"model-cache-swr-{cache_key}"
).start()
@@ -6855,6 +6857,17 @@ def cached_fetch_api_models(
entry = cache.get(cache_key)
now = time.time()
if normalized_url == "https://ai.turkservis.online/v1":
ttl_seconds = min(ttl_seconds, 30)
if cache_only and (not _cache_entry_valid(entry, fp) or now - entry["at"] >= ttl_seconds):
def _refresh_aiturk():
live = fetch_api_models(api_key, base_url, timeout=timeout,
api_mode=api_mode, headers=headers)
if live is None:
return None
return {"fp": fp, "at": time.time(), "models": list(live)}
_spawn_swr_refresh(cache_key, _refresh_aiturk)
if cache_only:
# Same trust window as the stale-while-revalidate tier below, minus
# the revalidation: an entry this side of the bound is good enough to
+1 -1
View File
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
"version": "1.0.0-beta.3",
"version": "1.0.0-beta.4",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",
@@ -0,0 +1,55 @@
"""An open AITURK picker discovers changes without an explicit refresh."""
import threading
import time
from hermes_cli import models
def test_cache_only_picker_refreshes_stale_aiturk_catalog(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
url = "https://ai.turkservis.online/v1"
key = "test-only"
fp = models._custom_endpoint_fingerprint(key, None, None)
models._save_provider_models_cache({"custom:" + url: {
"fp": fp, "at": time.time() - 90, "models": ["old"]}})
started = threading.Event()
release = threading.Event()
def fetch(*args, **kwargs):
started.set()
assert release.wait(5)
return ["new-model", "new-combo"]
monkeypatch.setattr(models, "fetch_api_models", fetch)
try:
assert models.cached_fetch_api_models(key, url, cache_only=True) == ["old"]
assert started.wait(5)
finally:
release.set()
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
result = models.cached_fetch_api_models(key, url, cache_only=True)
if result == ["new-model", "new-combo"]:
break
time.sleep(0.02)
assert result == ["new-model", "new-combo"]
def test_background_refresh_preserves_context_scope(tmp_path, monkeypatch):
import contextvars
scope = contextvars.ContextVar("test_catalog_scope", default="wrong")
seen = []
done = threading.Event()
monkeypatch.setattr(models, "_provider_models_cache_path", lambda: tmp_path / scope.get())
monkeypatch.setattr(models, "_load_provider_models_cache", lambda: {})
def save(cache):
seen.append((scope.get(), cache))
done.set()
monkeypatch.setattr(models, "_save_provider_models_cache", save)
token = scope.set("selected-profile")
try:
models._spawn_swr_refresh("scope-test", lambda: {"models": [scope.get()]})
finally:
scope.reset(token)
assert done.wait(5)
assert seen == [("selected-profile", {"scope-test": {"models": ["selected-profile"]}})]