jevify 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- jevify-0.3.0/.github/workflows/publish.yml +133 -0
- {jevify-0.2.0 → jevify-0.3.0}/PKG-INFO +12 -10
- {jevify-0.2.0 → jevify-0.3.0}/README.md +11 -9
- {jevify-0.2.0 → jevify-0.3.0}/SKILL.md +12 -5
- {jevify-0.2.0 → jevify-0.3.0}/docs/adr/0002-backend-port-and-adapter-kinds.md +22 -0
- jevify-0.3.0/docs/adr/0007-release-publishing.md +65 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/adr/README.md +1 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/playbook.md +5 -3
- {jevify-0.2.0 → jevify-0.3.0}/jevify/__init__.py +1 -1
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/endpoint/adapter.py +41 -12
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/endpoint/dialects.py +40 -14
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/endpoint/probe.py +78 -11
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/endpoint/readout.py +31 -8
- {jevify-0.2.0 → jevify-0.3.0}/jevify/ports/backend.py +3 -0
- {jevify-0.2.0 → jevify-0.3.0}/pyproject.toml +1 -1
- {jevify-0.2.0 → jevify-0.3.0}/tests/fakes/fake_openai_server.py +105 -26
- jevify-0.3.0/tests/fixtures/recipes/fake-tensorfold-messages.yaml +41 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/guards/test_request_fields.py +14 -0
- jevify-0.3.0/tests/test_prompt_logprobs_messages.py +149 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_prompt_logprobs_rung.py +0 -10
- {jevify-0.2.0 → jevify-0.3.0}/uv.lock +11 -11
- {jevify-0.2.0 → jevify-0.3.0}/.github/workflows/ci.yml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/.gitignore +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/AGENTS.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/LICENSE +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/Makefile +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/00-invariants.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/adr/0001-founding-choices.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/adr/0003-jev-compatible-api.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/adr/0004-recipes.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/adr/0005-evaluation-evidence-and-calibration.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/adr/0006-toolchain-dependency-budget-and-portability.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/bart-large-mnli.local--policy-29.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/bart-large-mnli.local--policy-29.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/bart-large-mnli.local--policy-hard-52.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/bart-large-mnli.local--policy-hard-52.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/deepseek-v4-flash-vision-exp-keys.vllm--calibration.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/deepseek-v4-flash-vision-exp-keys.vllm--calibration.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/deepseek-v4-flash-vision-exp-keys.vllm--doom-frames-41.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/deepseek-v4-flash-vision-exp-keys.vllm--doom-frames-41.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/deepseek-v4-flash-vision-exp-keys.vllm--long-state-24.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/deepseek-v4-flash-vision-exp-keys.vllm--long-state-24.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/deepseek-v4-flash-vision-exp-keys.vllm--policy-29.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/deepseek-v4-flash-vision-exp-keys.vllm--policy-29.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/deepseek-v4-flash-vision-exp-keys.vllm--policy-hard-52.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/deepseek-v4-flash-vision-exp-keys.vllm--policy-hard-52.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/gemma-4-e4b-it.llamacpp--doom-frames-41.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/gemma-4-e4b-it.llamacpp--doom-frames-41.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/gemma-4-e4b-it.llamacpp--long-state-24.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/gemma-4-e4b-it.llamacpp--long-state-24.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/gemma-4-e4b-it.llamacpp--policy-29.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/gemma-4-e4b-it.llamacpp--policy-29.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/gemma-4-e4b-it.llamacpp--policy-hard-52.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/gemma-4-e4b-it.llamacpp--policy-hard-52.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/qwen3-embedding-0.6b.local--policy-29.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/qwen3-embedding-0.6b.local--policy-29.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/qwen3-embedding-0.6b.local--policy-hard-52.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/qwen3-embedding-0.6b.local--policy-hard-52.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/qwen3-vl-reranker-2b.llamacpp--doom-frames-41.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/qwen3-vl-reranker-2b.llamacpp--doom-frames-41.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/qwen3-vl-reranker-2b.llamacpp--policy-29.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/qwen3-vl-reranker-2b.llamacpp--policy-29.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/qwen3-vl-reranker-2b.llamacpp--policy-hard-52.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/qwen3-vl-reranker-2b.llamacpp--policy-hard-52.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/qwen3-vl-reranker-8b.llamacpp--policy-29.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/qwen3-vl-reranker-8b.llamacpp--policy-29.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/ternary-bonsai-2-27b.llamacpp--doom-frames-41.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/ternary-bonsai-2-27b.llamacpp--doom-frames-41.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/ternary-bonsai-2-27b.llamacpp--long-state-24.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/ternary-bonsai-2-27b.llamacpp--long-state-24.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/ternary-bonsai-2-27b.llamacpp--policy-29.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/ternary-bonsai-2-27b.llamacpp--policy-29.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/ternary-bonsai-2-27b.llamacpp--policy-hard-52.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/docs/evidence/ternary-bonsai-2-27b.llamacpp--policy-hard-52.md +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/__main__.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/__init__.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/embedding/__init__.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/embedding/adapter.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/embedding/local.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/encoder/__init__.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/encoder/adapter.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/encoder/local.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/endpoint/__init__.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/endpoint/client.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/rerank/__init__.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/adapters/rerank/adapter.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/api/__init__.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/api/app.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/api/schema.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/api/translate.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/cli.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/compose.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/domain/__init__.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/domain/distribution.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/domain/engine.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/domain/questions.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/evaluation/__init__.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/evaluation/calibrate.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/evaluation/controls.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/evaluation/metrics.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/evaluation/scorecard.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/evaluation/suites.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/ports/__init__.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/recipes/__init__.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/recipes/probe_case.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/recipes/render.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/recipes/schema.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/jevify/recipes/store.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/recipes/bart-large-mnli.local.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/recipes/deepseek-v4-flash-vision-exp-keys.vllm.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/recipes/gemma-4-e4b-it.llamacpp.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/recipes/qwen3-embedding-0.6b.local.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/recipes/qwen3-vl-reranker-2b.llamacpp.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/recipes/qwen3-vl-reranker-8b.llamacpp.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/recipes/qwen38-27b.vllm.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/recipes/ternary-bonsai-2-27b.llamacpp.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/suites/doom-frames-41/doom-frames-41.jsonl +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/suites/doom-frames-41/doom-frames-41.manifest.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/suites/long-state-24.jsonl +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/suites/long-state-24.manifest.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/suites/policy-29.jsonl +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/suites/policy-29.manifest.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/suites/policy-hard-52.jsonl +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/suites/policy-hard-52.manifest.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/conftest.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/fakes/__init__.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/fakes/fake_backend.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/fixtures/recipes/fake-embedding.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/fixtures/recipes/fake-encoder.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/fixtures/recipes/fake-raw.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/fixtures/recipes/fake-rerank.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/fixtures/recipes/fake-tensorfold.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/fixtures/recipes/fake-vllm.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/fixtures/recipes/local-embedding.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/fixtures/recipes/local-encoder.yaml +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/fixtures/responses/llamacpp-completions-grammar.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/fixtures/responses/llamacpp-completions-top_k.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/fixtures/responses/llamacpp-native-completion.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/fixtures/responses/vllm-chat-top_k.json +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/guards/test_prompt_literals.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/guards/test_rung_registry.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/guards/test_sdk_schema_mirror.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/guards/test_structure.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/live/test_live_ask.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/live/test_live_sdk.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_api.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_calibrate.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_classify_and_states.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_cli.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_controls.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_distribution.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_embedding_adapter.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_encoder_adapter.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_endpoint_adapter.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_endpoint_client.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_engine.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_evaluation.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_llamacpp_dialect.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_local_embedding.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_multimodal.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_probe.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_readout_ladder.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_recipes.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_rerank_adapter.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_sdk_acceptance.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_translate.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tests/test_warm.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tools/author_long_state.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tools/author_policy_hard.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tools/play_doom.py +0 -0
- {jevify-0.2.0 → jevify-0.3.0}/tools/record_doom_suite.py +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# ADR-0007: a published GitHub release is built here from its tag and sent
|
|
2
|
+
# to PyPI by trusted publishing. No token is stored anywhere.
|
|
3
|
+
name: publish
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
release:
|
|
7
|
+
types: [published]
|
|
8
|
+
# Dry run against an existing tag: builds, checks and compares, never publishes.
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
inputs:
|
|
11
|
+
tag:
|
|
12
|
+
description: "Release tag to build and check, e.g. v0.2.0"
|
|
13
|
+
required: true
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
concurrency:
|
|
19
|
+
group: publish-${{ inputs.tag || github.event.release.tag_name }}
|
|
20
|
+
cancel-in-progress: false
|
|
21
|
+
|
|
22
|
+
env:
|
|
23
|
+
TAG: ${{ inputs.tag || github.event.release.tag_name }}
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
build:
|
|
27
|
+
name: build and check
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
outputs:
|
|
30
|
+
version: ${{ steps.version.outputs.version }}
|
|
31
|
+
attach: ${{ steps.compare.outputs.attach }}
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
with:
|
|
35
|
+
ref: refs/tags/${{ env.TAG }}
|
|
36
|
+
- uses: astral-sh/setup-uv@v6
|
|
37
|
+
with:
|
|
38
|
+
python-version: "3.12"
|
|
39
|
+
- name: Tag matches the package version
|
|
40
|
+
id: version
|
|
41
|
+
run: |
|
|
42
|
+
version="$(uv version --short)"
|
|
43
|
+
if [ "v${version}" != "${TAG}" ]; then
|
|
44
|
+
echo "::error::tag ${TAG} does not match version ${version} in pyproject.toml"
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
|
48
|
+
- name: Build
|
|
49
|
+
run: uv build
|
|
50
|
+
- name: twine check
|
|
51
|
+
run: uvx twine check --strict dist/*
|
|
52
|
+
- name: Install the wheel as a tool and run it from another directory
|
|
53
|
+
env:
|
|
54
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
55
|
+
run: |
|
|
56
|
+
uv tool install dist/*.whl
|
|
57
|
+
cd "${RUNNER_TEMP}"
|
|
58
|
+
got="$(jevify --version)"
|
|
59
|
+
if [ "${got}" != "jevify ${VERSION}" ]; then
|
|
60
|
+
echo "::error::installed wheel reports '${got}', expected 'jevify ${VERSION}'"
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
- name: Compare with the files attached to the release
|
|
64
|
+
id: compare
|
|
65
|
+
env:
|
|
66
|
+
GH_TOKEN: ${{ github.token }}
|
|
67
|
+
GH_REPO: ${{ github.repository }}
|
|
68
|
+
run: |
|
|
69
|
+
attached_names="$(gh release view "${TAG}" --json assets \
|
|
70
|
+
-q '.assets[].name | select(endswith(".whl") or endswith(".tar.gz"))')"
|
|
71
|
+
if [ -z "${attached_names}" ]; then
|
|
72
|
+
echo "No wheel or sdist attached to ${TAG}; this build is attached after publishing."
|
|
73
|
+
echo "attach=true" >> "$GITHUB_OUTPUT"
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
dir="${RUNNER_TEMP}/attached"
|
|
77
|
+
mkdir -p "${dir}"
|
|
78
|
+
gh release download "${TAG}" -p '*.whl' -p '*.tar.gz' -D "${dir}"
|
|
79
|
+
built="$(cd dist && sha256sum -- *.whl *.tar.gz)"
|
|
80
|
+
attached="$(cd "${dir}" && sha256sum -- *.whl *.tar.gz)"
|
|
81
|
+
if [ "${built}" != "${attached}" ]; then
|
|
82
|
+
echo "::error::the files attached to ${TAG} differ from this build of the tag"
|
|
83
|
+
echo "built:"; echo "${built}"
|
|
84
|
+
echo "attached:"; echo "${attached}"
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
echo "Attached files are byte-identical to this build:"
|
|
88
|
+
echo "${built}"
|
|
89
|
+
echo "attach=false" >> "$GITHUB_OUTPUT"
|
|
90
|
+
- uses: actions/upload-artifact@v4
|
|
91
|
+
with:
|
|
92
|
+
name: dist
|
|
93
|
+
path: dist/
|
|
94
|
+
if-no-files-found: error
|
|
95
|
+
|
|
96
|
+
publish:
|
|
97
|
+
name: publish to PyPI
|
|
98
|
+
needs: build
|
|
99
|
+
if: github.event_name == 'release'
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
environment:
|
|
102
|
+
name: pypi
|
|
103
|
+
url: https://pypi.org/project/jevify/${{ needs.build.outputs.version }}/
|
|
104
|
+
permissions:
|
|
105
|
+
id-token: write
|
|
106
|
+
steps:
|
|
107
|
+
- uses: actions/download-artifact@v4
|
|
108
|
+
with:
|
|
109
|
+
name: dist
|
|
110
|
+
path: dist/
|
|
111
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
112
|
+
|
|
113
|
+
attach:
|
|
114
|
+
name: attach build to the release
|
|
115
|
+
needs: [build, publish]
|
|
116
|
+
if: github.event_name == 'release' && needs.build.outputs.attach == 'true'
|
|
117
|
+
runs-on: ubuntu-latest
|
|
118
|
+
permissions:
|
|
119
|
+
contents: write
|
|
120
|
+
steps:
|
|
121
|
+
- uses: actions/download-artifact@v4
|
|
122
|
+
with:
|
|
123
|
+
name: dist
|
|
124
|
+
path: dist/
|
|
125
|
+
- name: Upload wheel, sdist and SHA256SUMS
|
|
126
|
+
env:
|
|
127
|
+
GH_TOKEN: ${{ github.token }}
|
|
128
|
+
GH_REPO: ${{ github.repository }}
|
|
129
|
+
run: |
|
|
130
|
+
cd dist
|
|
131
|
+
sha256sum -- *.whl *.tar.gz > SHA256SUMS
|
|
132
|
+
cat SHA256SUMS
|
|
133
|
+
gh release upload "${TAG}" *.whl *.tar.gz SHA256SUMS
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: jevify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Make a pretrained model you already have answer typed questions in one pass, and measure how well it does.
|
|
5
5
|
Project-URL: Homepage, https://github.com/fidecastro/jevify
|
|
6
6
|
Author: Felipe Infante de Castro
|
|
@@ -89,9 +89,11 @@ that model and writes what it found into the file. vLLM and llama.cpp get
|
|
|
89
89
|
their dialect extras; any other server is driven through the plain OpenAI
|
|
90
90
|
surface. A server that refuses logprobs on the reply token but returns
|
|
91
91
|
vLLM's `prompt_logprobs`, such as TensorFold's native server with
|
|
92
|
-
GLM-5.3-Flash, is read through the `prompt_logprobs` rung
|
|
93
|
-
raw-mode recipe (the template written out with the
|
|
94
|
-
tokens,
|
|
92
|
+
GLM-5.3-Flash, is read through the `prompt_logprobs` rung on
|
|
93
|
+
`/v1/completions`: with a raw-mode recipe (the template written out with the
|
|
94
|
+
model's own control tokens), or with a chat-template recipe where the server
|
|
95
|
+
also takes `messages` on that route, as TensorFold's does. The chat form is
|
|
96
|
+
the one that carries images.
|
|
95
97
|
|
|
96
98
|
`serve` binds to `127.0.0.1`. To reach it from other machines pass
|
|
97
99
|
`--host 0.0.0.0`, and set `JEVIFY_API_KEY` first so that every request must
|
|
@@ -287,12 +289,12 @@ dies before the armour, so the corridor needs a policy with memory, not
|
|
|
287
289
|
better perception); the rerank kind and the
|
|
288
290
|
embedding route against a live server; a marker-slot encoder layout; the
|
|
289
291
|
grammar rung on the PrismML llama.cpp fork, which accepts a grammar without
|
|
290
|
-
constraining the reported probabilities; the `prompt_logprobs` rung
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
every question
|
|
295
|
-
notes); Windows. Choice menus on the endpoint kind are bounded by the
|
|
292
|
+
constraining the reported probabilities; the `prompt_logprobs` rung on any
|
|
293
|
+
server but TensorFold's native one with GLM-5.3-Flash, where it has been
|
|
294
|
+
probed and asked by hand (raw and chat recipes, text and images) but no
|
|
295
|
+
evaluation suite has been scored on it yet (that server skips its prefix
|
|
296
|
+
cache for such requests, so every question reads the whole state again,
|
|
297
|
+
images included, which the probe measures and notes); Windows. Choice menus on the endpoint kind are bounded by the
|
|
296
298
|
single-token identifier alphabet the probe verifies (26 on the DeepSeek
|
|
297
299
|
recipe); Jev's 255-option ceiling is not reachable there.
|
|
298
300
|
|
|
@@ -59,9 +59,11 @@ that model and writes what it found into the file. vLLM and llama.cpp get
|
|
|
59
59
|
their dialect extras; any other server is driven through the plain OpenAI
|
|
60
60
|
surface. A server that refuses logprobs on the reply token but returns
|
|
61
61
|
vLLM's `prompt_logprobs`, such as TensorFold's native server with
|
|
62
|
-
GLM-5.3-Flash, is read through the `prompt_logprobs` rung
|
|
63
|
-
raw-mode recipe (the template written out with the
|
|
64
|
-
tokens,
|
|
62
|
+
GLM-5.3-Flash, is read through the `prompt_logprobs` rung on
|
|
63
|
+
`/v1/completions`: with a raw-mode recipe (the template written out with the
|
|
64
|
+
model's own control tokens), or with a chat-template recipe where the server
|
|
65
|
+
also takes `messages` on that route, as TensorFold's does. The chat form is
|
|
66
|
+
the one that carries images.
|
|
65
67
|
|
|
66
68
|
`serve` binds to `127.0.0.1`. To reach it from other machines pass
|
|
67
69
|
`--host 0.0.0.0`, and set `JEVIFY_API_KEY` first so that every request must
|
|
@@ -257,12 +259,12 @@ dies before the armour, so the corridor needs a policy with memory, not
|
|
|
257
259
|
better perception); the rerank kind and the
|
|
258
260
|
embedding route against a live server; a marker-slot encoder layout; the
|
|
259
261
|
grammar rung on the PrismML llama.cpp fork, which accepts a grammar without
|
|
260
|
-
constraining the reported probabilities; the `prompt_logprobs` rung
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
every question
|
|
265
|
-
notes); Windows. Choice menus on the endpoint kind are bounded by the
|
|
262
|
+
constraining the reported probabilities; the `prompt_logprobs` rung on any
|
|
263
|
+
server but TensorFold's native one with GLM-5.3-Flash, where it has been
|
|
264
|
+
probed and asked by hand (raw and chat recipes, text and images) but no
|
|
265
|
+
evaluation suite has been scored on it yet (that server skips its prefix
|
|
266
|
+
cache for such requests, so every question reads the whole state again,
|
|
267
|
+
images included, which the probe measures and notes); Windows. Choice menus on the endpoint kind are bounded by the
|
|
266
268
|
single-token identifier alphabet the probe verifies (26 on the DeepSeek
|
|
267
269
|
recipe); Jev's 255-option ceiling is not reachable there.
|
|
268
270
|
|
|
@@ -81,7 +81,9 @@ headers, headers_env}`, `template` and `answers`.
|
|
|
81
81
|
own control tokens; use it when the chat template would not put the model
|
|
82
82
|
in its trained regime (the Qwen3-VL reranker recipe is the example), and
|
|
83
83
|
for a server that refuses reply-token logprobs (TensorFold's native server
|
|
84
|
-
with GLM-5.3-Flash)
|
|
84
|
+
with GLM-5.3-Flash) when the state is text. The `prompt_logprobs` rung also
|
|
85
|
+
reads a messages-mode recipe where the server takes `messages` on
|
|
86
|
+
`/v1/completions` (TensorFold's does); that is the form that carries images.
|
|
85
87
|
- `template.questions.choice_strategy`: `identifiers` (one request, options
|
|
86
88
|
labelled A, B, C) or `per_option` (one yes/no request per option; needed
|
|
87
89
|
for a model that never emits letters, such as a reranker).
|
|
@@ -90,7 +92,8 @@ headers, headers_env}`, `template` and `answers`.
|
|
|
90
92
|
- `readout.rung: auto` walks the rungs the probe proved, strongest first:
|
|
91
93
|
`named_token_logprobs` (vLLM with `logprob_token_ids`), `grammar`
|
|
92
94
|
(llama.cpp), `top_k`, `prompt_logprobs` (each answer spelling appended to
|
|
93
|
-
a raw prompt
|
|
95
|
+
a raw prompt, or sent as the assistant's reply in messages mode, and read
|
|
96
|
+
back from vLLM-style `prompt_logprobs` rows, one
|
|
94
97
|
request more for every spelling the rows did not show; exact, off-menu mass
|
|
95
98
|
visible), `equal_bias`, `top_k_floor` (degraded, missing labels floored and
|
|
96
99
|
listed). A rung that cannot see every label hands the
|
|
@@ -167,9 +170,13 @@ TypeSafe's published thresholds refer to. Everything jevify adds is under
|
|
|
167
170
|
user turn right after the state. Use `template.mode: raw` so the warm prompt
|
|
168
171
|
ends at the state boundary; the Bonsai recipe is the example.
|
|
169
172
|
- **Probe notes "reply-token logprobs refused"**: the server gives no
|
|
170
|
-
logprobs on a generated token (TensorFold with GLM-5.3-Flash).
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
logprobs on a generated token (TensorFold with GLM-5.3-Flash). The probe
|
|
174
|
+
then tries `prompt_logprobs`: a raw recipe on any such server, a chat
|
|
175
|
+
recipe where the server takes `messages` on `/v1/completions`; elsewhere a
|
|
176
|
+
chat recipe ends in "no readout is possible".
|
|
177
|
+
- **"is not at the prompt row the probe measured"**: in messages mode the
|
|
178
|
+
server's template renders the answer turn differently than when the probe
|
|
179
|
+
ran (thinking switched on or off, a new template); re-run the probe.
|
|
173
180
|
- **"did not come back as its own prompt token"**: the appended answer fuses
|
|
174
181
|
with the end of the raw template; end the template where the answer starts
|
|
175
182
|
a token of its own (after a newline, or before a leading-space spelling).
|
|
@@ -158,6 +158,28 @@ that request twice and records when the repeat reuses no cached prefix,
|
|
|
158
158
|
since TensorFold skips its prefix cache for any request with
|
|
159
159
|
`prompt_logprobs` and each question then reads the whole state again.
|
|
160
160
|
|
|
161
|
+
Amended 2026-11-06, for images on that server: a raw prompt carries no
|
|
162
|
+
image, so the rung above left such a server text-only. TensorFold's
|
|
163
|
+
`/v1/completions` also takes a chat `messages` list, image parts included,
|
|
164
|
+
renders it through the model's template and serves `prompt_logprobs` on it
|
|
165
|
+
(its GLM prefill encodes the images and records the prompt rows in one
|
|
166
|
+
pass). In messages mode the rung therefore sends the readout there, with the
|
|
167
|
+
answer spelling as the assistant's reply. The template always appends its
|
|
168
|
+
generation prompt, so the answer's row is not the last: the probe sends the
|
|
169
|
+
reply twice with two different answers, requires the rows to differ in
|
|
170
|
+
exactly one position that holds each answer, and records the rows after it
|
|
171
|
+
as `prompt_rows_after_answer`. A search for the answer's token from the end
|
|
172
|
+
was rejected, because it can find the same word in the question. Each
|
|
173
|
+
readout reads that row and refuses one that does not hold the appended
|
|
174
|
+
answer, naming a re-probe: a template that renders differently later (thinking
|
|
175
|
+
switched) fails loudly instead of being read at the wrong position. When the
|
|
176
|
+
strongest proven rung reads prompt rows, the warm request and the probe's
|
|
177
|
+
cache, fan-out and image measurements take the same route, so the image is
|
|
178
|
+
proven where questions will send it and a server that refuses reply-token
|
|
179
|
+
logprobs on the chat route never gets one. A server that takes no `messages`
|
|
180
|
+
on `/v1/completions` (vLLM) leaves the rung unproven for a chat recipe, as
|
|
181
|
+
before.
|
|
182
|
+
|
|
161
183
|
### D5 — Choice questions on decoders
|
|
162
184
|
|
|
163
185
|
Options are labeled with single-token identifiers and the distribution is
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# ADR-0007 — Releases reach PyPI from CI, by trusted publishing
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted (2026-11-04).
|
|
4
|
+
- **Extends:** ADR-0006 D4 (build and distribution); records who builds the
|
|
5
|
+
files that go to PyPI and how they get there.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
0.1.0 and 0.2.0 were built by hand with `uv build` from their tags, attached
|
|
10
|
+
to the GitHub release with a `SHA256SUMS`, and uploaded to PyPI with a token.
|
|
11
|
+
The upload step needed a PyPI token on the operator's machine, and the house
|
|
12
|
+
rule keeps secrets out of files, logs and chat. A rebuild of `v0.2.0` from a
|
|
13
|
+
fresh clone produced files byte-identical to the ones attached to the
|
|
14
|
+
release, so the build is reproducible and a second build can check the first.
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
|
|
18
|
+
### D1 — Publishing a GitHub release publishes to PyPI
|
|
19
|
+
|
|
20
|
+
`.github/workflows/publish.yml` runs when a release is published. It checks
|
|
21
|
+
out the release tag, refuses a tag that is not `v` + the version in
|
|
22
|
+
`pyproject.toml`, builds with `uv build`, runs `twine check --strict`, and
|
|
23
|
+
installs the wheel as a tool and runs `jevify --version`.
|
|
24
|
+
|
|
25
|
+
### D2 — Trusted publishing, no stored token
|
|
26
|
+
|
|
27
|
+
The upload uses PyPI trusted publishing (OIDC) from the `publish` job in the
|
|
28
|
+
GitHub environment `pypi`. No PyPI token exists in the repository, its
|
|
29
|
+
secrets or the operator's machine. The PyPI project's trusted publisher names
|
|
30
|
+
this repository, the workflow file `publish.yml` and the environment `pypi`;
|
|
31
|
+
renaming either breaks publishing until PyPI is updated.
|
|
32
|
+
|
|
33
|
+
### D3 — The CI build must match what the release shows
|
|
34
|
+
|
|
35
|
+
If the release already carries a wheel and an sdist, the CI build must be
|
|
36
|
+
byte-identical to them or the workflow stops before uploading. If it carries
|
|
37
|
+
none, the CI build is uploaded to PyPI and then attached to the release with
|
|
38
|
+
a `SHA256SUMS`. Either way the files on PyPI and on the GitHub release are
|
|
39
|
+
the same bytes. Files are attached while the release is a draft, before it
|
|
40
|
+
is published, so the workflow never races a half-attached release.
|
|
41
|
+
|
|
42
|
+
### D4 — Dry run
|
|
43
|
+
|
|
44
|
+
`workflow_dispatch` with a `tag` runs the same build and checks against an
|
|
45
|
+
existing release without publishing.
|
|
46
|
+
|
|
47
|
+
## Consequences
|
|
48
|
+
|
|
49
|
+
- Cutting a release is: bump the version, merge, tag, then publish a GitHub
|
|
50
|
+
release from the tag. Publishing it is the PyPI upload; there is no
|
|
51
|
+
separate step.
|
|
52
|
+
- Each job holds only the permission it needs: `contents: read` to build,
|
|
53
|
+
`id-token: write` to publish, `contents: write` to attach.
|
|
54
|
+
- A non-reproducible build (a new hatchling changing metadata, say) shows up
|
|
55
|
+
as a mismatch in D3 when files were attached by hand. The remedy is to let
|
|
56
|
+
CI attach them.
|
|
57
|
+
|
|
58
|
+
## Rejected alternatives
|
|
59
|
+
|
|
60
|
+
- **An API token in repository secrets.** It works, but it is a long-lived
|
|
61
|
+
secret that trusted publishing makes unnecessary.
|
|
62
|
+
- **Uploading the hand-attached files as they are.** PyPI would carry files
|
|
63
|
+
that no CI run built; D3 keeps hand-built files allowed but checked.
|
|
64
|
+
- **Publishing on tag push.** A tag is not a release note; the GitHub release
|
|
65
|
+
is the reviewed announcement, so it is the trigger.
|
|
@@ -18,3 +18,4 @@ later documents can cite "ADR-0002 D3".
|
|
|
18
18
|
| [0004](0004-recipes.md) | Recipes: the hashed file that is the only way a model is asked | Accepted (2026-09-19) |
|
|
19
19
|
| [0005](0005-evaluation-evidence-and-calibration.md) | Suites, scorecards, committed evidence, post-hoc calibration | Accepted (2026-09-19) |
|
|
20
20
|
| [0006](0006-toolchain-dependency-budget-and-portability.md) | Toolchain, dependency budget and portability: five light core packages, extras, argparse, uv/pipx, Python 3.12–3.14, no lock-in | Accepted (2026-09-19); makes ADR-0001 D4 precise |
|
|
21
|
+
| [0007](0007-release-publishing.md) | Releases reach PyPI from CI by trusted publishing; the CI build must match the release files | Accepted (2026-11-04) |
|
|
@@ -229,8 +229,10 @@ two ways:
|
|
|
229
229
|
do; any recipe works;
|
|
230
230
|
- for the prompt (vLLM's `prompt_logprobs` on `/v1/completions`), as
|
|
231
231
|
TensorFold's native server does for GLM-5.3-Flash, which refuses the
|
|
232
|
-
first way; the
|
|
233
|
-
|
|
232
|
+
first way; the probe proves the `prompt_logprobs` rung. A raw-mode recipe
|
|
233
|
+
works on any such server. A chat-template recipe works where the server
|
|
234
|
+
also takes a chat `messages` list on `/v1/completions` (TensorFold's does),
|
|
235
|
+
and is the one that can send images there.
|
|
234
236
|
|
|
235
237
|
Many hosted APIs and proxies do neither; for those the probe stops with
|
|
236
238
|
`no readout is possible`.
|
|
@@ -288,7 +290,7 @@ calibrated.
|
|
|
288
290
|
| You see | Cause | Do this |
|
|
289
291
|
|---|---|---|
|
|
290
292
|
| `missing required header 'x-client-id'` | The server needs a header the recipe does not send. | Add `endpoint.headers: {x-client-id: <your product id>}`. |
|
|
291
|
-
| `no readout is possible on this backend: it returned no logprobs` | The server or proxy returns no log-probabilities for the reply token, and the recipe could not read them through `prompt_logprobs`. | If the probe notes `reply-token logprobs refused`, write a raw-mode recipe so it can prove `prompt_logprobs` (step 6)
|
|
293
|
+
| `no readout is possible on this backend: it returned no logprobs` | The server or proxy returns no log-probabilities for the reply token, and the recipe could not read them through `prompt_logprobs`. | If the probe notes `reply-token logprobs refused`, write a raw-mode recipe so it can prove `prompt_logprobs` (step 6); a chat-template recipe proves it only where the server takes `messages` on `/v1/completions`. Otherwise use a server that returns them (vLLM, llama.cpp). |
|
|
292
294
|
| `endpoint.headers names X-Api-Key, which looks like a credential` | A secret would be written into the recipe. | Move it to `endpoint.headers_env: {X-Api-Key: <VARIABLE>}` and export the variable. |
|
|
293
295
|
| `endpoint.headers_env sends X-Api-Key from $GATEWAY_KEY, which is not set` | The variable the recipe names is not exported in this shell. | `export GATEWAY_KEY=...`, then run again. |
|
|
294
296
|
| `degraded: true`, options listed in `missing` | The options were not among the model's most likely tokens. Often the model starts to reason instead of answering. | Turn reasoning off (step 6, item 5), then probe again. |
|
|
@@ -25,7 +25,7 @@ from jevify.ports.backend import (
|
|
|
25
25
|
Rung,
|
|
26
26
|
StateHandle,
|
|
27
27
|
)
|
|
28
|
-
from jevify.recipes.render import render_prefix, render_question
|
|
28
|
+
from jevify.recipes.render import RenderedPart, render_prefix, render_question
|
|
29
29
|
from jevify.recipes.schema import Recipe
|
|
30
30
|
|
|
31
31
|
Clock = Callable[[], float]
|
|
@@ -92,8 +92,12 @@ class EndpointBackend:
|
|
|
92
92
|
slots = tuple(range(min(self.concurrency, self.slots))) if self.slots else ()
|
|
93
93
|
started = self.clock()
|
|
94
94
|
cached: int | None = None
|
|
95
|
+
# the route the questions will take, so a server that refuses one never sees it
|
|
96
|
+
prompt_rows = self._reads_prompt_rows()
|
|
95
97
|
for slot in slots or (None,):
|
|
96
|
-
path, body = base_request(
|
|
98
|
+
path, body = base_request(
|
|
99
|
+
self.recipe.model.name, prefix, "", self.dialect, prompt_rows=prompt_rows
|
|
100
|
+
)
|
|
97
101
|
if slot is not None:
|
|
98
102
|
body["id_slot"] = slot
|
|
99
103
|
post = self.client.post_root if path == "completion" else self.client.post_v1
|
|
@@ -130,6 +134,35 @@ class EndpointBackend:
|
|
|
130
134
|
proven = Capabilities.from_dict(self.recipe.probe).rungs if self.recipe.probe else set()
|
|
131
135
|
return [r for r in RUNG_RANK if r in proven and r in RUNGS]
|
|
132
136
|
|
|
137
|
+
def _reads_prompt_rows(self) -> bool:
|
|
138
|
+
"""Whether the strongest rung reads prompt rows; False before a probe on auto."""
|
|
139
|
+
if self.recipe.readout.rung == "auto" and not self.recipe.probe:
|
|
140
|
+
return False
|
|
141
|
+
return RUNGS[self._resolve_rung()].reads_prompt_rows
|
|
142
|
+
|
|
143
|
+
def _readout_request(
|
|
144
|
+
self, handle: StateHandle, part: RenderedPart, impl: Any, slot: int | None, tally: _Tally
|
|
145
|
+
) -> tuple[str, dict[str, Any], Send]:
|
|
146
|
+
"""The one way a question's readout request is built: the rung's route, and the
|
|
147
|
+
sender that reads the answer's row where the rung reads prompt rows."""
|
|
148
|
+
path, body = base_request(
|
|
149
|
+
self.recipe.model.name,
|
|
150
|
+
handle.prefix,
|
|
151
|
+
part.text,
|
|
152
|
+
self.dialect,
|
|
153
|
+
prompt_rows=impl.reads_prompt_rows,
|
|
154
|
+
)
|
|
155
|
+
answer_row = -1
|
|
156
|
+
if impl.reads_prompt_rows and handle.prefix.mode == "messages":
|
|
157
|
+
after = self.capabilities.prompt_rows_after_answer if self.capabilities else None
|
|
158
|
+
if after is None:
|
|
159
|
+
raise BackendError(
|
|
160
|
+
"the prompt_logprobs rung in messages mode reads the row the probe "
|
|
161
|
+
"measured (probe.prompt_rows_after_answer); run `jevify probe <recipe>`"
|
|
162
|
+
)
|
|
163
|
+
answer_row = -1 - after
|
|
164
|
+
return path, body, self._sender(path, slot, tally, answer_row)
|
|
165
|
+
|
|
133
166
|
def _resolve_rung(self) -> Rung:
|
|
134
167
|
pinned = self.recipe.readout.rung
|
|
135
168
|
proven = (
|
|
@@ -189,11 +222,9 @@ class EndpointBackend:
|
|
|
189
222
|
raw_parts = []
|
|
190
223
|
for order in orders:
|
|
191
224
|
part = render_question(self.recipe, question, order=order).parts[0]
|
|
192
|
-
path, body =
|
|
193
|
-
self.recipe.model.name, handle.prefix, part.text, self.dialect
|
|
194
|
-
)
|
|
225
|
+
path, body, send = self._readout_request(handle, part, impl, slot, tally)
|
|
195
226
|
parsed = await impl.collect(
|
|
196
|
-
|
|
227
|
+
send,
|
|
197
228
|
body,
|
|
198
229
|
part,
|
|
199
230
|
self.recipe.readout.top_k,
|
|
@@ -232,7 +263,7 @@ class EndpointBackend:
|
|
|
232
263
|
raw={"parts": raw_parts},
|
|
233
264
|
)
|
|
234
265
|
|
|
235
|
-
def _sender(self, path: str, slot: int | None, tally: _Tally) -> Send:
|
|
266
|
+
def _sender(self, path: str, slot: int | None, tally: _Tally, answer_row: int) -> Send:
|
|
236
267
|
"""The one way a readout request goes out: pinned to its slot, timed, counted."""
|
|
237
268
|
post = self.client.post_root if path == "completion" else self.client.post_v1
|
|
238
269
|
|
|
@@ -243,7 +274,7 @@ class EndpointBackend:
|
|
|
243
274
|
response = await post(path, body)
|
|
244
275
|
tally.latency_ms += (self.clock() - started) * 1000.0
|
|
245
276
|
tally.calls += 1
|
|
246
|
-
return parse_response(path, response, self.dialect)
|
|
277
|
+
return parse_response(path, response, self.dialect, answer_row=answer_row)
|
|
247
278
|
|
|
248
279
|
return send
|
|
249
280
|
|
|
@@ -263,11 +294,9 @@ class EndpointBackend:
|
|
|
263
294
|
cached_tokens: int | None = None
|
|
264
295
|
raw_parts = []
|
|
265
296
|
for part in rendered.parts:
|
|
266
|
-
path, body =
|
|
267
|
-
self.recipe.model.name, handle.prefix, part.text, self.dialect
|
|
268
|
-
)
|
|
297
|
+
path, body, send = self._readout_request(handle, part, impl, slot, tally)
|
|
269
298
|
parsed = await impl.collect(
|
|
270
|
-
|
|
299
|
+
send,
|
|
271
300
|
body,
|
|
272
301
|
part,
|
|
273
302
|
self.recipe.readout.top_k,
|
|
@@ -34,12 +34,16 @@ class Parsed:
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def base_request(
|
|
37
|
-
model: str, prefix: RenderedPrefix, suffix: str, dialect: Dialect
|
|
37
|
+
model: str, prefix: RenderedPrefix, suffix: str, dialect: Dialect, *, prompt_rows: bool = False
|
|
38
38
|
) -> tuple[str, dict[str, Any]]:
|
|
39
39
|
"""The readout request every rung starts from: one token, no sampling, logprobs on.
|
|
40
40
|
|
|
41
41
|
Returns the route key and the body: "chat/completions" and "completions" live under
|
|
42
42
|
/v1; "completion" is llama.cpp's native root route, used for raw multimodal input.
|
|
43
|
+
|
|
44
|
+
`prompt_rows`: the readout reads prompt rows, not the reply (the prompt_logprobs rung).
|
|
45
|
+
A messages-mode body then goes to /v1/completions, where TensorFold's native server takes
|
|
46
|
+
a chat `messages` list and serves `prompt_logprobs`, and asks for no reply logprobs.
|
|
43
47
|
"""
|
|
44
48
|
if prefix.mode == "raw":
|
|
45
49
|
if prefix.images:
|
|
@@ -87,13 +91,17 @@ def base_request(
|
|
|
87
91
|
"stream": False,
|
|
88
92
|
"logprobs": True,
|
|
89
93
|
}
|
|
94
|
+
if prompt_rows:
|
|
95
|
+
del body["logprobs"]
|
|
90
96
|
if prefix.prefill:
|
|
97
|
+
# on prompt rows the answer is appended to this message (the rung's shape)
|
|
91
98
|
messages.append({"role": "assistant", "content": prefix.prefill})
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
if not prompt_rows:
|
|
100
|
+
body["continue_final_message"] = True
|
|
101
|
+
body["add_generation_prompt"] = False
|
|
94
102
|
if prefix.kwargs and dialect is not Dialect.GENERIC:
|
|
95
103
|
body["chat_template_kwargs"] = dict(prefix.kwargs)
|
|
96
|
-
return "chat/completions", body
|
|
104
|
+
return ("completions" if prompt_rows else "chat/completions"), body
|
|
97
105
|
|
|
98
106
|
|
|
99
107
|
def _base64_payload(data_uri: str) -> str:
|
|
@@ -113,7 +121,11 @@ def grammar_for(texts: list[str]) -> str:
|
|
|
113
121
|
return f"root ::= {escaped}"
|
|
114
122
|
|
|
115
123
|
|
|
116
|
-
def parse_response(
|
|
124
|
+
def parse_response(
|
|
125
|
+
path: str, response: dict[str, Any], dialect: Dialect, *, answer_row: int = -1
|
|
126
|
+
) -> Parsed:
|
|
127
|
+
"""`answer_row`: which prompt_logprobs row is the answer's, counted from the end (-1: the
|
|
128
|
+
last, a raw prompt; earlier where the template adds rows after an assistant reply)."""
|
|
117
129
|
entries: list[Entry] = []
|
|
118
130
|
if path == "completion":
|
|
119
131
|
# llama.cpp native route: completion_probabilities plus top-level counters.
|
|
@@ -131,16 +143,10 @@ def parse_response(path: str, response: dict[str, Any], dialect: Dialect) -> Par
|
|
|
131
143
|
usage = response.get("usage") or {}
|
|
132
144
|
details = usage.get("prompt_tokens_details") or {}
|
|
133
145
|
rows = choice.get("prompt_logprobs")
|
|
134
|
-
if isinstance(rows, list) and rows and isinstance(rows[
|
|
146
|
+
if isinstance(rows, list) and len(rows) >= -answer_row and isinstance(rows[answer_row], dict):
|
|
135
147
|
# vLLM's prompt_logprobs (TensorFold writes the same): null, then one {id: {logprob,
|
|
136
|
-
# rank, decoded_token}} row a prompt token, the prompt's own token listed first.
|
|
137
|
-
|
|
138
|
-
row = [
|
|
139
|
-
Entry(
|
|
140
|
-
text=str(item.get("decoded_token", "")), logprob=float(item["logprob"]), id=int(key)
|
|
141
|
-
)
|
|
142
|
-
for key, item in rows[-1].items()
|
|
143
|
-
]
|
|
148
|
+
# rank, decoded_token}} row a prompt token, the prompt's own token listed first.
|
|
149
|
+
row = _row_entries(rows[answer_row])
|
|
144
150
|
return Parsed(
|
|
145
151
|
entries=tuple(row),
|
|
146
152
|
prompt_tokens=usage.get("prompt_tokens"),
|
|
@@ -170,6 +176,26 @@ def parse_response(path: str, response: dict[str, Any], dialect: Dialect) -> Par
|
|
|
170
176
|
)
|
|
171
177
|
|
|
172
178
|
|
|
179
|
+
def _row_entries(row: dict[str, Any]) -> list[Entry]:
|
|
180
|
+
return [
|
|
181
|
+
Entry(text=str(item.get("decoded_token", "")), logprob=float(item["logprob"]), id=int(key))
|
|
182
|
+
for key, item in row.items()
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def prompt_tokens_held(response: dict[str, Any]) -> tuple[Entry | None, ...]:
|
|
187
|
+
"""Each prompt_logprobs row's own token (its first entry), None for the first row: the
|
|
188
|
+
prompt as the server tokenized it. Empty when the response lists no prompt rows."""
|
|
189
|
+
rows = response["choices"][0].get("prompt_logprobs")
|
|
190
|
+
if not isinstance(rows, list):
|
|
191
|
+
return ()
|
|
192
|
+
held: list[Entry | None] = []
|
|
193
|
+
for row in rows:
|
|
194
|
+
entries = _row_entries(row) if isinstance(row, dict) and row else []
|
|
195
|
+
held.append(entries[0] if entries else None)
|
|
196
|
+
return tuple(held)
|
|
197
|
+
|
|
198
|
+
|
|
173
199
|
def _entry_from(item: dict[str, Any], dialect: Dialect) -> Entry:
|
|
174
200
|
text = str(item.get("token", ""))
|
|
175
201
|
ident = item.get("id")
|