Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
237d1e2060 | ||
|
|
aa498f4cbb |
+4
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ test('a package upgrade refreshes only a proven managed runtime, once per packag
|
||||
const stamp = {commit: 'b'.repeat(40), source: 'git'}
|
||||
assert.equal(needsPackagedRuntimeUpgrade(true, stamp, VALID_MARKER), true)
|
||||
assert.equal(needsPackagedRuntimeUpgrade(true, stamp, {...VALID_MARKER, pinnedCommit: stamp.commit}), false)
|
||||
assert.equal(needsPackagedRuntimeUpgrade(true, stamp, {...VALID_MARKER, pinnedCommit: 'c'.repeat(40), packageCommit: stamp.commit}), false)
|
||||
assert.equal(needsPackagedRuntimeUpgrade(false, stamp, VALID_MARKER), false)
|
||||
assert.equal(needsPackagedRuntimeUpgrade(true, stamp, null), false)
|
||||
assert.equal(needsPackagedRuntimeUpgrade(true, {commit: '0'.repeat(40), source: 'fallback'}, VALID_MARKER), false)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface BootstrapMarkerLike {
|
||||
pinnedCommit?: unknown
|
||||
packageCommit?: unknown
|
||||
schemaVersion?: unknown
|
||||
}
|
||||
|
||||
@@ -12,7 +13,7 @@ export function needsPackagedRuntimeUpgrade(
|
||||
): boolean {
|
||||
return Boolean(packaged && stamp?.source !== 'fallback' &&
|
||||
/^[0-9a-f]{40}$/i.test(stamp?.commit || '') && !/^0+$/.test(stamp?.commit || '') &&
|
||||
hasValidBootstrapMarker(marker, 1) && marker?.pinnedCommit !== stamp?.commit)
|
||||
hasValidBootstrapMarker(marker, 1) && (marker?.packageCommit || marker?.pinnedCommit) !== stamp?.commit)
|
||||
}
|
||||
|
||||
export interface ActiveRuntimeState {
|
||||
|
||||
@@ -143,8 +143,8 @@ test('resolveMarkerPinnedCommit prefers real HEAD over fallback stamp zeros', ()
|
||||
resolveMarkerPinnedCommit({ commit: 'd'.repeat(40), branch: 'main' }, '/tmp/checkout', {
|
||||
resolveHead: () => realHead
|
||||
}),
|
||||
'd'.repeat(40),
|
||||
'packaged real pin wins over checkout HEAD'
|
||||
realHead,
|
||||
'marker reports the actual installed checkout, including a preserved newer runtime'
|
||||
)
|
||||
assert.equal(
|
||||
resolveMarkerPinnedCommit({ commit: ZERO_COMMIT, branch: 'main' }, '/tmp/missing', {
|
||||
|
||||
@@ -111,16 +111,16 @@ function resolveMarkerPinnedCommit(
|
||||
): string | null {
|
||||
const resolveHead = opts.resolveHead || resolveCheckoutHead
|
||||
|
||||
if (installStamp && isPinnedCommit(installStamp.commit)) {
|
||||
return installStamp.commit
|
||||
}
|
||||
|
||||
const head = resolveHead(activeRoot)
|
||||
|
||||
if (head) {
|
||||
return head
|
||||
}
|
||||
|
||||
if (installStamp && isPinnedCommit(installStamp.commit)) {
|
||||
return installStamp.commit
|
||||
}
|
||||
|
||||
return readExistingPinnedCommit(activeRoot)
|
||||
}
|
||||
|
||||
@@ -918,14 +918,14 @@ async function runBootstrap(opts) {
|
||||
|
||||
try {
|
||||
const existingCheckout = hasExistingGitCheckout(activeRoot)
|
||||
const pinCommit = !existingCheckout
|
||||
const pinCommit = Boolean(installStamp && isPinnedCommit(installStamp.commit)) || !existingCheckout
|
||||
|
||||
if (existingCheckout && installStamp && installStamp.commit) {
|
||||
emit({
|
||||
type: 'log',
|
||||
line:
|
||||
`[bootstrap] existing checkout detected at ${activeRoot}; ` +
|
||||
`not pinning to packaged install stamp ${installStamp.commit.slice(0, 12)}`
|
||||
`applying packaged install stamp ${installStamp.commit.slice(0, 12)} with installer rollback protection`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1002,6 +1002,7 @@ async function runBootstrap(opts) {
|
||||
|
||||
const markerPayload = {
|
||||
pinnedCommit,
|
||||
packageCommit: installStamp && isPinnedCommit(installStamp.commit) ? installStamp.commit : null,
|
||||
pinnedBranch: installStamp ? installStamp.branch : null
|
||||
}
|
||||
|
||||
|
||||
@@ -4597,6 +4597,7 @@ function writeBootstrapMarker(payload) {
|
||||
const merged = {
|
||||
schemaVersion: BOOTSTRAP_MARKER_SCHEMA_VERSION,
|
||||
pinnedCommit: payload.pinnedCommit || null,
|
||||
packageCommit: payload.packageCommit || null,
|
||||
pinnedBranch: payload.pinnedBranch || null,
|
||||
completedAt: new Date().toISOString(),
|
||||
desktopVersion: app.getVersion()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "aiturk-ide",
|
||||
"productName": "AITURK IDE",
|
||||
"private": true,
|
||||
"version": "1.0.0-beta.2",
|
||||
"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)
|
||||
})
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Generated
+1
-1
@@ -65,7 +65,7 @@
|
||||
},
|
||||
"apps/desktop": {
|
||||
"name": "aiturk-ide",
|
||||
"version": "1.0.0-beta.2",
|
||||
"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"]}})]
|
||||
Reference in New Issue
Block a user