Skip to content

Commit 2e6395c

Browse files
author
sagitchu
committed
fix: delete non-native gem & auto create gems
1 parent 9ffab48 commit 2e6395c

6 files changed

Lines changed: 53 additions & 88 deletions

File tree

README.md

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,25 +145,19 @@ When a gem is selected:
145145

146146
- The server uses the gem definition's `model` as the actual Gemini model name for the upstream call.
147147
- The OpenAI-compatible response tries to echo back the client-provided `model` (i.e. `gem:<id>`) to keep client-side consistency.
148+
- **Auto-Sync**: On startup, the server checks if these gems exist on your Google account. If missing, it automatically creates them using the provided `id` (as title) and `system_prompt`.
148149

149150
Example config:
150151

151152
```yaml
152153
gemini:
153154
gems:
154-
- id: "default-gem"
155-
model: "gemini-3.0-pro"
156-
system_prompt: "You are a careful assistant."
155+
- id: "coding-helper"
156+
model: "gemini-2.0-flash"
157+
system_prompt: "You are an expert software engineer."
157158
tool_policy: "allow" # allow | disallow | auto
158159
default_temperature: 0.2
159160
top_p: 0.8
160-
max_output_tokens: 1024
161-
162-
# Native Gem (Use Gems created on gemini.google.com)
163-
- id: "my-native-gem"
164-
model: "gemini-2.0-flash"
165-
is_native: true # This flag passes the 'id' to Google's servers
166-
default_temperature: 1.0 # Parameters can still be overridden locally
167161
max_output_tokens: 8192
168162
```
169163

README.zh.md

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,25 +145,19 @@ docker compose up -d
145145

146146
- 服务端会使用 gem 定义中的 `model` 作为实际调用的 Gemini 模型名。
147147
- 对外返回的 OpenAI 兼容响应会尽量回显客户端传入的 `model`(即 `gem:<id>`),以保证客户端一致性。
148+
- **自动同步**:服务启动时会自动检查这些 Gem 是否存在于你的 Google 账号中。如果不存在,会自动根据提供的 `id`(作为标题)和 `system_prompt` 进行创建。
148149

149150
配置示例:
150151

151152
```yaml
152153
gemini:
153154
gems:
154-
- id: "default-gem"
155-
model: "gemini-3.0-pro"
156-
system_prompt: "You are a careful assistant."
155+
- id: "coding-helper"
156+
model: "gemini-2.0-flash"
157+
system_prompt: "You are an expert software engineer."
157158
tool_policy: "allow" # allow | disallow | auto
158159
default_temperature: 0.2
159160
top_p: 0.8
160-
max_output_tokens: 1024
161-
162-
# 原生 Gem (使用在 gemini.google.com 上创建的 Gems)
163-
- id: "my-native-gem"
164-
model: "gemini-2.0-flash"
165-
is_native: true # 此标志会将 'id' 传递给 Google 服务器
166-
default_temperature: 1.0 # 仍然可以在本地覆盖参数
167161
max_output_tokens: 8192
168162
```
169163

app/server/chat.py

Lines changed: 2 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -108,59 +108,6 @@ def _resolve_gem_from_model_or_400(
108108
return gem_id, gem, public_model, gem.model
109109

110110

111-
def _apply_gem_overrides(
112-
request_obj: Any,
113-
gem: GemDefinition,
114-
) -> None:
115-
"""Apply gem overrides onto a request object in-place.
116-
117-
Notes:
118-
- This helper intentionally does NOT mutate `request_obj.model`.
119-
- Upstream model selection is handled via `model=gem:<id>` parsing.
120-
"""
121-
122-
if hasattr(request_obj, "temperature"):
123-
request_obj.temperature = gem.default_temperature
124-
125-
if gem.top_p is not None and hasattr(request_obj, "top_p"):
126-
request_obj.top_p = gem.top_p
127-
128-
# ChatCompletions uses `max_tokens`, Responses uses `max_output_tokens`.
129-
if hasattr(request_obj, "max_tokens"):
130-
request_obj.max_tokens = gem.max_output_tokens
131-
if hasattr(request_obj, "max_output_tokens"):
132-
request_obj.max_output_tokens = gem.max_output_tokens
133-
134-
if gem.tool_policy == "disallow":
135-
if hasattr(request_obj, "tools"):
136-
request_obj.tools = None
137-
if hasattr(request_obj, "tool_choice"):
138-
request_obj.tool_choice = "none"
139-
elif gem.tool_policy == "auto":
140-
# Placeholder for future expansion.
141-
pass
142-
# Default behavior is `allow` (no changes).
143-
144-
145-
def _inject_gem_system_prompt(messages: list[Message], system_prompt: str) -> None:
146-
"""Inject a gem system prompt before model preparation.
147-
148-
The prompt is inserted as the first system message (or prepended to an
149-
existing first system message) so it applies to the whole conversation.
150-
"""
151-
152-
if not system_prompt:
153-
return
154-
155-
if messages and messages[0].role == "system" and isinstance(messages[0].content, str):
156-
existing = messages[0].content or ""
157-
separator = "\n\n" if existing else ""
158-
messages[0].content = f"{system_prompt}{separator}{existing}"
159-
return
160-
161-
messages.insert(0, Message(role="system", content=system_prompt))
162-
163-
164111
def _build_structured_requirement(
165112
response_format: dict[str, Any] | None,
166113
) -> StructuredOutputRequirement | None:
@@ -704,12 +651,7 @@ async def create_chat_completion(
704651
gem_id, gem, public_model, actual_model = _resolve_gem_from_model_or_400(request.model)
705652
logger.info(f"[DEBUG_GEM] Resolved: gem_id={gem_id}, actual_model={actual_model}")
706653

707-
if gem:
708-
_apply_gem_overrides(request, gem)
709-
if gem.system_prompt:
710-
_inject_gem_system_prompt(request.messages, gem.system_prompt)
711-
712-
native_gem_id = gem_id if (gem and gem.is_native) else None
654+
native_gem_id = gem_id if gem else None
713655

714656
try:
715657
model = Model.from_name(actual_model)
@@ -982,10 +924,8 @@ async def create_response(
982924
base_messages, normalized_input = _response_items_to_messages(request_data.input)
983925

984926
gem_id, gem, public_model, actual_model = _resolve_gem_from_model_or_400(request_data.model)
985-
if gem:
986-
_apply_gem_overrides(request_data, gem)
987927

988-
native_gem_id = gem_id if (gem and gem.is_native) else None
928+
native_gem_id = gem_id if gem else None
989929

990930
structured_requirement = _build_structured_requirement(request_data.response_format)
991931
if structured_requirement and request_data.stream:
@@ -1029,8 +969,6 @@ async def create_response(
1029969
logger.debug("Image generation support enabled for /v1/responses request.")
1030970

1031971
preface_messages = _instructions_to_messages(request_data.instructions)
1032-
if gem and gem.system_prompt:
1033-
_inject_gem_system_prompt(preface_messages, gem.system_prompt)
1034972

1035973
conversation_messages = base_messages
1036974
if preface_messages:

app/services/client.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,45 @@ async def init(
8181
logger.exception(f"Failed to initialize GeminiClient {self.id}")
8282
raise
8383

84+
async def sync_gems(self) -> None:
85+
"""Ensure configured gems exist on the server."""
86+
from ..utils import g_config
87+
88+
if not g_config.gemini.gems:
89+
return
90+
91+
try:
92+
gem_jar = await self.fetch_gems()
93+
# GemJar behaves like a dict-like object where keys are ID and values are Gem objects
94+
# Or it might be a list. Let's assume we can iterate it.
95+
# To be safe against unknown structure, we'll try to inspect one if possible or just rely on 'title' attribute.
96+
97+
# Since we can't easily debug, let's look at the probe output again.
98+
# It has 'gems' attribute on client too. Maybe that's cached?
99+
100+
existing_titles = set()
101+
if gem_jar:
102+
# Assuming gem_jar is iterable yielding Gem objects
103+
for g in gem_jar:
104+
# Try to find the name/title attribute
105+
title = getattr(g, "title", getattr(g, "name", None))
106+
if title:
107+
existing_titles.add(title)
108+
109+
for gem_def in g_config.gemini.gems:
110+
if gem_def.id not in existing_titles:
111+
logger.info(f"Creating missing gem for client {self.id}: {gem_def.id}")
112+
await self.create_gem(
113+
name=gem_def.id,
114+
prompt=gem_def.system_prompt or "",
115+
description=f"Auto-generated gem for {gem_def.id}",
116+
)
117+
else:
118+
logger.debug(f"Gem already exists: {gem_def.id}")
119+
120+
except Exception as e:
121+
logger.error(f"Failed to sync gems for client {self.id}: {e}")
122+
84123
def running(self) -> bool:
85124
return self._running
86125

app/services/pool.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ async def init(self) -> None:
4545
verbose=g_config.gemini.verbose,
4646
refresh_interval=g_config.gemini.refresh_interval,
4747
)
48+
if client.running():
49+
await client.sync_gems()
4850
except Exception:
4951
logger.exception(f"Failed to initialize client {client.id}")
5052

@@ -97,6 +99,8 @@ async def _ensure_client_ready(self, client: GeminiClientWrapper) -> bool:
9799
verbose=g_config.gemini.verbose,
98100
refresh_interval=g_config.gemini.refresh_interval,
99101
)
102+
if client.running():
103+
await client.sync_gems()
100104
logger.info(f"Restarted Gemini client {client.id} after it stopped.")
101105
return True
102106
except Exception:

app/utils/config.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,6 @@ class GemDefinition(BaseModel):
5555

5656
id: str = Field(..., description="Unique identifier for the gem")
5757
model: str = Field(..., description="Model name to use when this gem is selected")
58-
is_native: bool = Field(
59-
default=False,
60-
description="If True, this gem ID is passed upstream to Google's servers. If False, it is a local configuration preset.",
61-
)
6258
system_prompt: Optional[str] = Field(
6359
default=None,
6460
description="Optional system prompt injected before user messages",

0 commit comments

Comments
 (0)