Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_TIMEOUT_SECONDS=30
# POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_BASE_URL=http://127.0.0.1:8081/v1
# POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_HEADERS={"Authorization":"Bearer replace-me"}
# POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_MODEL_SETTINGS={"dimensions":2560}
# Fixed-dimension models reject the dimensions request field. Keep EMBEDDING_DIMENSION and disable sending:
# POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_SEND_DIMENSIONS=false
# A dedicated reranker LLM is optional; without it, reranking reuses the generation model.
# POWERCONTEXT_SERVER_INFERENCE_RERANK_MODEL=openai-chat:local-reranker
# POWERCONTEXT_SERVER_INFERENCE_RERANK_BASE_URL=http://127.0.0.1:8082/v1
Expand All @@ -139,6 +141,7 @@ POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_DIMENSION=1536
# POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_MODEL=openai:your-embedding-model
# POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_PROFILE_ID=your-embedding-model-dimension-unit-v1
# POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_DIMENSION=1024
# POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_SEND_DIMENSIONS=false
# OPENAI_BASE_URL=https://your-openai-compatible-service/v1
# Set OPENAI_API_KEY in the Server shell when the service requires authentication.

Expand Down
5 changes: 4 additions & 1 deletion docs/en/development/pydantic-ai-inference.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ the selected Pydantic AI provider.

The Server rejects a partial embedding profile. `embedding_model`, `embedding_profile_id`, and `embedding_dimension`
must be configured together. SQLite vector search uses that embedding configuration because the index dimension and
stored vectors must agree.
stored vectors must agree. `embedding_dimension` is also sent as the `dimensions` request field by default, and it
overrides a `dimensions` value in model settings. For a fixed-dimension model that rejects the field, set
`POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_SEND_DIMENSIONS` to `false`. The local dimension stays required, and returned
vectors are still validated against it.

## Compose generation directly

Expand Down
3 changes: 2 additions & 1 deletion docs/en/docs/operate/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ Server settings use the `POWERCONTEXT_SERVER_` prefix.
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_HEADERS` | `{}` | JSON object of static embedding client headers; values are secrets |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_MODEL_SETTINGS` | `{}` | JSON object of Pydantic AI embedding model settings |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_PROFILE_ID` | unset | Stable identity for the model, dimension, and normalization used by the vector index |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_DIMENSION` | unset | Positive output dimension requested from and validated against the embedding model |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_DIMENSION` | unset | Positive output dimension stored and validated. Also sent as the `dimensions` request field by default |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_SEND_DIMENSIONS` | `true` | Whether embedding requests include `dimensions`. When false, returned vectors are still checked against the configured dimension. Set false for fixed-dimension models such as SiliconFlow `BAAI/bge-m3` |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_NORMALIZATION` | `unit` | Vector normalization: `unit` or `none` |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_TIMEOUT_SECONDS` | `30` | Timeout in seconds for one embedding request |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_BATCH_SIZE` | `10` | Maximum texts sent in one embedding request |
Expand Down
8 changes: 8 additions & 0 deletions docs/en/docs/workflows/configure-vector-search.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ export POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_DIMENSION=1024
Replace the example values with one supported provider model, a profile ID you keep stable for that model, and its
documented output dimension. `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_NORMALIZATION` defaults to `unit`.

The output dimension is always required. It fixes the local vector index width and validates returned vectors. By
default that value is also sent as the `dimensions` request field. Some OpenAI-compatible services reject the field,
including SiliconFlow `BAAI/bge-m3`. Keep the documented output dimension (1024 for bge-m3) and disable sending:

```bash
export POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_SEND_DIMENSIONS=false
```

## 2. Start the Server

```bash
Expand Down
3 changes: 3 additions & 0 deletions docs/zh/development/pydantic-ai-inference.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ timeout 和 request limit 未显式设置时继承 generation 的对应配置。

Server 会拒绝不完整的 embedding profile。`embedding_model`、`embedding_profile_id` 和
`embedding_dimension` 必须一起配置。SQLite vector search 使用这组配置,因为 index dimension 必须与持久化向量一致。
`embedding_dimension` 默认也会作为请求参数 `dimensions` 发送,并覆盖 model settings 里的同名字段。固定输出维度、
不接受该参数的模型把 `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_SEND_DIMENSIONS` 设为 `false`;本地维度仍然必填,
返回向量仍按它校验。

## 直接组合 generation

Expand Down
3 changes: 2 additions & 1 deletion docs/zh/docs/operate/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ Server 配置使用 `POWERCONTEXT_SERVER_` 前缀。
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_HEADERS` | `{}` | embedding client 静态 header JSON object;value 按 secret 处理 |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_MODEL_SETTINGS` | `{}` | Pydantic AI embedding model settings JSON object |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_PROFILE_ID` | 未设置 | vector index 使用的模型、dimension 和 normalization 的稳定标识 |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_DIMENSION` | 未设置 | 向 embedding model 请求并校验的正整数输出维度 |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_DIMENSION` | 未设置 | 持久化并校验的正整数输出维度。默认也作为请求参数 `dimensions` 发送 |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_SEND_DIMENSIONS` | `true` | 是否在 embedding 请求中发送 `dimensions`。关闭后仍用上面的维度校验返回向量。固定维度模型(例如硅基流动 `BAAI/bge-m3`)设为 `false` |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_NORMALIZATION` | `unit` | vector normalization:`unit` 或 `none` |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_TIMEOUT_SECONDS` | `30` | 单次 embedding 请求的超时秒数 |
| `POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_BATCH_SIZE` | `10` | 单次 embedding 请求最多发送的文本数量 |
Expand Down
6 changes: 6 additions & 0 deletions docs/zh/docs/workflows/configure-vector-search.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_DIMENSION=1024
将示例值替换为一个受支持的 provider model、该模型保持稳定的 profile ID,以及其文档给出的输出 dimension。
`POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_NORMALIZATION` 默认值为 `unit`。

输出维度始终要配置,它决定本地向量索引的宽度,并用来校验模型返回的向量。默认还会把这个值作为请求参数 `dimensions` 发给模型。有些 OpenAI 兼容服务不接受该参数,例如硅基流动上的 `BAAI/bge-m3`。这类模型保留文档中的输出维度(bge-m3 为 1024),并关闭发送:

```bash
export POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_SEND_DIMENSIONS=false
```

## 2. 启动 Server

```bash
Expand Down
13 changes: 12 additions & 1 deletion src/powercontext/builtin/runtime/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,17 @@ def _usage_reporting_embedding_model(model: EmbeddingModel | None) -> EmbeddingM
return UsageReportingEmbeddingModel(model)


def _embedding_provider_settings(settings: InferenceConfig, dimension: int) -> dict[str, JsonValue]:
"""Return embedding request settings, keeping stored dimension separate from the wire field."""

request_settings = dict(settings.embedding_model_settings)
if settings.embedding_send_dimensions:
request_settings["dimensions"] = dimension
else:
request_settings.pop("dimensions", None)
return request_settings


async def _embedding_models(
settings: InferenceConfig,
resources: AsyncExitStack,
Expand Down Expand Up @@ -1463,7 +1474,7 @@ def provider_factory(provider_name: str) -> Provider[Any]:
limits = InferenceLimits(timeout_seconds=settings.embedding_timeout_seconds)
embedding_settings = cast(
EmbeddingSettings,
settings.embedding_model_settings | {"dimensions": profile.dimension},
_embedding_provider_settings(settings, profile.dimension),
)

def adapter(instrument: InstrumentationSettings | bool | None) -> EmbeddingModel:
Expand Down
1 change: 1 addition & 0 deletions src/powercontext/builtin/runtime/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ class InferenceConfig(BaseModel):
embedding_model_settings: dict[str, JsonValue] = Field(default_factory=dict)
embedding_profile_id: str | None = None
embedding_dimension: int | None = Field(default=None, ge=1)
embedding_send_dimensions: bool = True
embedding_normalization: Literal["none", "unit"] = "unit"
embedding_timeout_seconds: float = Field(default=30.0, gt=0)
embedding_batch_size: int = Field(default=10, ge=1)
Expand Down
36 changes: 35 additions & 1 deletion tests/builtin/runtime/test_composition_embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,18 @@ def __init__(self, model, *, settings=None, defer_model_check=True, instrument=N
type(self).settings_seen.append(settings)


def _patch_embedder(monkeypatch: pytest.MonkeyPatch) -> None:
"""Replace Embedder only for one test, including the adapter's already-bound name."""

import powercontext.builtin.inference.pydantic_ai as adapter

monkeypatch.setattr("pydantic_ai.Embedder", _SpyEmbedder)
monkeypatch.setattr(adapter, "Embedder", _SpyEmbedder)


def test_embedding_models_send_the_configured_dimension_to_the_provider(monkeypatch) -> None:
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
monkeypatch.setattr("pydantic_ai.Embedder", _SpyEmbedder)
_patch_embedder(monkeypatch)
_SpyEmbedder.settings_seen = []

async def scenario() -> None:
Expand All @@ -62,6 +71,31 @@ async def scenario() -> None:
assert readiness is not None
expected_settings = {"dimensions": 1536, "extra_body": {"route": "embedding"}}
assert _SpyEmbedder.settings_seen == [expected_settings, expected_settings]
assert operational.profile.dimension == 1536

asyncio.run(scenario())


def test_embedding_models_omit_dimensions_when_sending_is_disabled(monkeypatch) -> None:
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
_patch_embedder(monkeypatch)
_SpyEmbedder.settings_seen = []

async def scenario() -> None:
config = InferenceConfig(
embedding_model="openai:BAAI/bge-m3",
embedding_model_settings={"dimensions": 512, "extra_body": {"route": "embedding"}},
embedding_profile_id="bge-m3-1024-unit",
embedding_dimension=1024,
embedding_send_dimensions=False,
)
async with AsyncExitStack() as resources:
operational, readiness = await _embedding_models(config, resources, None)
assert operational is not None
assert readiness is not None
assert operational.profile.dimension == 1024
expected_settings = {"extra_body": {"route": "embedding"}}
assert _SpyEmbedder.settings_seen == [expected_settings, expected_settings]

asyncio.run(scenario())

Expand Down
37 changes: 37 additions & 0 deletions tests/test_inference_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,40 @@ async def scenario() -> None:
assert "rerank-secret" not in log_output

asyncio.run(scenario())


def test_embedding_requests_omit_dimensions_when_sending_is_disabled(tmp_path: Path) -> None:
async def scenario() -> None:
with _model_server() as (embedding_server, embedding_url):
config = BuiltinConfig(
database=SQLiteConfig(url=f"sqlite+aiosqlite:///{tmp_path / 'runtime.db'}"),
inference=InferenceConfig(
embedding_model="openai:BAAI/bge-m3",
embedding_base_url=AnyHttpUrl(embedding_url),
embedding_model_settings={"dimensions": 99, "extra_body": {"route": "embedding"}},
embedding_profile_id="bge-m3-3-unit",
embedding_dimension=3,
embedding_send_dimensions=False,
),
)
async with open_builtin_runtime(config) as runtime:
assert runtime.scopes is not None
scope = await runtime.scopes.create(
ScopeDraft(
title="Fixed dimension",
summary="Embedding requests omit dimensions",
idempotency_key="omit-dimensions",
)
)
await runtime.memory.for_scope(scope.scope_id).remember(
RememberMemoryRequest(entries=(MemoryEntryInput(kind="fact", text="bge-m3 stores 1024 values."),))
)

assert embedding_server.requests
for embedding_request in embedding_server.requests:
assert embedding_request["path"] == "/v1/embeddings"
assert embedding_request["body"]["model"] == "BAAI/bge-m3"
assert "dimensions" not in embedding_request["body"]
assert embedding_request["body"]["route"] == "embedding"

asyncio.run(scenario())
3 changes: 3 additions & 0 deletions tests/test_server_embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def test_embedding_settings_load_one_complete_environment_profile(
monkeypatch.setenv("POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_MODEL", " test-provider:test-model ")
monkeypatch.setenv("POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_PROFILE_ID", " test-profile-v1 ")
monkeypatch.setenv("POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_DIMENSION", "3")
monkeypatch.setenv("POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_SEND_DIMENSIONS", "false")
monkeypatch.setenv("POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_NORMALIZATION", " unit ")
monkeypatch.setenv("POWERCONTEXT_SERVER_INFERENCE_EMBEDDING_BATCH_SIZE", "7")

Expand All @@ -35,13 +36,15 @@ def test_embedding_settings_load_one_complete_environment_profile(
assert settings.inference.embedding_model == "test-provider:test-model"
assert settings.inference.embedding_profile_id == "test-profile-v1"
assert settings.inference.embedding_dimension == 3
assert settings.inference.embedding_send_dimensions is False
assert settings.inference.embedding_normalization == "unit"
assert settings.inference.embedding_batch_size == 7


def test_embedding_normalization_defaults_to_unit() -> None:
assert InferenceConfig().embedding_normalization == "unit"
assert InferenceConfig().embedding_batch_size == 10
assert InferenceConfig().embedding_send_dimensions is True


def test_embedding_settings_reject_unknown_normalization() -> None:
Expand Down
Loading