This document describes the mounted v2 API surface under /v2.
/v2/extract (both GET and POST) and /v2/jobs/{job_id} require a
bearer token in the Authorization header. /v2/health is the only v2
endpoint that stays open.
Authorization: Bearer <API_TOKEN>The expected token comes from the server-side API_TOKEN env var
(generate with python -c "import secrets; print(secrets.token_urlsafe(32))").
The check uses hmac.compare_digest for constant-time comparison.
| Condition | Status | Notes |
|---|---|---|
API_TOKEN unset on the server |
503 |
Fails closed; no dev bypass. |
| Header missing | 401 |
Includes WWW-Authenticate: Bearer. |
| Wrong token | 401 |
Same response shape as missing header. |
| Valid token | route's normal response |
(The legacy /v1/* routes were removed in 3.0.0.)
Implementation lives in git_metadata_extractor/auth.py (verify_token dependency).
Disabled by default. Set V2_RATE_LIMIT_PER_MINUTE to a positive integer to
cap requests per client on the compute-/cost-heavy routes:
POST /v2/extractandPOST /v2/indices/{provider}/ingestGET /v2/extract/{full_path}
Clients are keyed by bearer token (falling back to client IP). Exceeding the
limit returns 429 Too Many Requests with a Retry-After header. The window
is a fixed 60 s. State is in-memory and per worker, so under gunicorn with
N workers the effective limit is ~N × the configured value — use a shared store
(e.g. Redis) for a true global cap. Implementation: git_metadata_extractor/rate_limit.py.
Returns service readiness for v2 dependencies. Open (no auth required).
Example:
curl -s "http://localhost:1234/v2/health" | jqRuns the v2 extraction pipeline for a GitHub URL path.
Repository-mode traversal contract:
- GitHub traversal is direct-only: source repository, direct owner, and direct contributors.
- GitHub repo-list expansion (
/users/{u}/repos,/orgs/{o}/repos) is disabled. - ORCID, Infoscience, and ROR enrichment remain enabled for discovered people/organizations.
pulse:ownsemitted during repository runs is constrained to the source repository handle when present.
Query parameters:
output_format:jsonld(default) orjsonagent_runtime:rule_based|llm(optional; defaults toV2_AGENT_RUNTIME_DEFAULT=llm)agent_runtime=llmruns LLM agents for repository/user/organization roots and fanout stages.- In LLM runtime, two global fail-open stages are always enabled:
llm_dedup(post-permissive, pre-reconciliation): LLM duplicate-cluster suggestions + deterministic constrained merge/remap.llm_critic(post-reconciliation, pre-strict, gated byV2_APPLY_CRITIC_PRUNING): LLM prune suggestions + deterministic non-root pruning/cascade cleanup.
- Both stages are warning-only on failure (pipeline continues).
- Root-stage hard-fail policy in LLM mode (no rule-based fallback):
- repository input:
repo_agent - user input:
person_agent - organization input:
org_agent
- repository input:
include_context_summary:true|false(defaultfalse, includes the compiled LLM context summary markdown used by downstream agents when available)
Examples:
curl -s -H "Authorization: Bearer $API_TOKEN" \
"http://localhost:1234/v2/extract/github.com/octocat/Hello-World?output_format=jsonld" \
| jqcurl -s -H "Authorization: Bearer $API_TOKEN" \
"http://localhost:1234/v2/extract/github.com/octocat/Hello-World?output_format=json&agent_runtime=rule_based" \
| jqcurl -s -H "Authorization: Bearer $API_TOKEN" \
"http://localhost:1234/v2/extract/github.com/octocat/Hello-World?output_format=json&agent_runtime=llm" \
| jqSubmits an extraction asynchronously. The handler validates the URL synchronously, creates a pending job in the ProviderCache-backed job store, schedules the pipeline as a background task, and returns 202 Accepted with a job_id. Poll GET /v2/jobs/{job_id} to retrieve the result. GET /v2/extract/{full_path} remains synchronous and unchanged.
Request body:
source_url(required): GitHub path or URL (for examplegithub.com/octocat/Hello-World)output_format:jsonld|json(defaultjsonld)agent_runtime:rule_based|llm(optional)include_context_summary:true|false(defaultfalse)
Response (202 Accepted):
job_id: opaque UUID to pollstatus:pendingstatus_url: convenience path (/v2/jobs/{job_id}) for the companion GETsubmitted_at: ISO-8601 timestamp
Error responses:
422 Unprocessable Entityifsource_urlis unsupported (returned synchronously, no job is created).503 Service Unavailableif the provider cache is disabled — the async job store has no backing storage.
Example:
curl -s -X POST "http://localhost:1234/v2/extract" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"source_url": "github.com/octocat/Hello-World",
"output_format": "json",
"agent_runtime": "llm",
"include_context_summary": true
}' | jq{
"job_id": "5d2b8b3d-3e6e-4a82-9b17-1c2f5b6e7a31",
"status": "pending",
"status_url": "/v2/jobs/5d2b8b3d-3e6e-4a82-9b17-1c2f5b6e7a31",
"submitted_at": "2026-04-29T10:31:00.000Z"
}Companion retrieval endpoint for jobs submitted via POST /v2/extract. Returns the persisted V2ExtractJob record with the latest status.
Response fields:
job_idstatus:pending|running|completed|failedrequest: the originalV2ExtractRequestpayload (withsource_urlnormalized)submitted_at,started_at,completed_atresult: present only whenstatus == "completed". SameV2ExtractResponsecontract asGET /v2/extract/{full_path}(source_url,detected_type,output_format,output,warnings,stats, optionalcontext_summary_markdown).error: present only whenstatus == "failed". SameV2ErrorResponseshape as the error responses onGET /v2/extract/{full_path}(error_type,detail,source_url, optionalerrors[]).
Status responses:
200 OK— record found (any status).404 Not Found— no job exists with that id (also returned for jobs that have aged past theProviderCacheTTL).503 Service Unavailable— provider cache disabled, job store unavailable.
Example:
curl -s -H "Authorization: Bearer $API_TOKEN" \
"http://localhost:1234/v2/jobs/5d2b8b3d-3e6e-4a82-9b17-1c2f5b6e7a31" | jqPersistence: jobs share the SQLite-backed ProviderCache (V2_PROVIDER_CACHE_PATH, TTL V2_PROVIDER_CACHE_TTL_DAYS) under the v2-extract-job namespace. V2_PROVIDER_CACHE_ENABLED=false disables the job store entirely.
/v2/extract runs the same pipeline regardless of agent_runtime. The
runtime only controls which agent implementations execute (LLM agents vs.
deterministic rule-based agents). Other stages run unconditionally.
1. classify_url classify the input as repository / user / org
2. gather_context fetch GitHub metadata + GIMIE JSON-LD
3. context_summary [LLM] compile a markdown summary; raw blobs are stripped
from per-agent prompts in LLM mode
4. repo_agent produce the root repository entity
5. person_agents (fan-out) one agent per discovered contributor / user
6. org_agents (fan-out) one agent per discovered organisation
7. article_agents (fan-out) discover scholarly articles tied to the repo
8. membership_agents (fan-out) one agent per (person, org) pair
9. contribution_agents (fan-out) one agent per (person, repo) pair
10. llm_dedup [LLM] cross-bucket entity dedup + ID remap (fail-open)
11. reconcile_entities deterministic ID canonicalisation + linkage
12. llm_critic [LLM, gated] drop-suggestion stage (off by default)
13. guarantee_repo_author stamp github-owner as schema:author when empty
14. strict_validation per-entity strict JSON Schema check
15. assemble_output split graph into root + related + excluded
16. link_veracity [LLM, gated] verify every URL via Selenium fetch + LLM
17. validate_articles drop placeholder / sentinel-DOI articles
18. validate_author_classes drop `schema:author` refs whose target is
not a `schema:Person`
19. validate_ownership strip mismatched pulse:owns
20. infer_owners stamp pulse:owns / pulse:ownedBy from handles;
coerces residual bare-login strings on
`pulse:ownedBy` to `{"@id": "https://github.com/{handle}"}`
21. infer_github_handle_parents fuzzy-search ROR for parent of every github
org; add ROR org entities, stamp unitOf
22. org_relationships [LLM] whole-graph LLM call to refine unitOf edges
23. infer_org_units deterministic name-token fallback for unitOf
24. build_jsonld_output produce the final JSON-LD graph; strips
redundant `pulse:ror` from any
`org:Organization` whose `@id` is already
the ROR (closed-shape fix)
Gates:
- Stages tagged
[LLM]only run inagent_runtime=llm. link_veracityis[LLM]-only: inagent_runtime=rule_basedit is always skipped (rule-based mode is guaranteed LLM-free).llm_criticis off by default. SetV2_APPLY_CRITIC_PRUNING=trueto enable (LLM mode only).link_veracityis on by default in LLM mode. SetV2_LINK_VERACITY_ENABLED=falseto skip even in LLM mode (recommended for batch runs).
link_veracity behavior:
- Scans all discovered HTTP(S) links across assembled entities.
- For articles, validates
schema:identifieras DOI (bare DOI strings are normalized tohttps://doi.org/<doi>). - Removes links explicitly fetched and marked unreachable.
- Removes entities when their canonical URL/DOI fails validation, or when no valid URL remains.
- Checker/runtime errors are fail-open (warnings only) and do not auto-prune.
/v2/extract returns:
source_urldetected_typeoutput_formatoutputwarningsstats- optional
context_summary_markdown(wheninclude_context_summary=trueand a compiled summary exists)
output_format=json contract (output):
root_entity: object | nullrelated_entities: list[object]excluded_entities: list[object]entities_by_type: {repositories, persons, organizations, articles, memberships, contributions}
output_format=jsonld contract (output):
@context: object@graph: list[object]- optional
excluded_entities(only when present)
Each entity bucket has a dedicated agent under git_metadata_extractor/agents/llm/<kind>/agent.py
(repository, person, organization, article, membership, contribution).
Each is a single pydantic-ai Agent call that produces all output
fields in one prompt/response round-trip. Hallucination guards baked into
agents:
force_server_uuidoverwrites whatever UUID the LLM emitted with a server-generated one inidentifiers.uuidonly.- Repository agent post-LLM: stars/forks from GitHub REST (deterministic);
discipline fallback
wd:Q428691(computer engineering) when the LLM emits empty/null. - Contribution agent post-LLM: stamps
schema:authorandpulse:contributionTofrom the orchestrator's authoritative pair. - Article agent post-LLM: drops the entity if
schema:identifieris a placeholder DOI (10.0000/...) or sentinel string (UNKNOWN,N/A,TBD, …) and nopulse:infoscienceArticleIdentifieris present.
Token counts (tokens_prompt, tokens_completion) are extracted from
result.usage() (pydantic-ai 1.5+ exposes usage as a method on
AgentRunResult, not a property — the V2LLMRuntime resolves this with
a callable guard).
LLM agents can call server-side tools during generation. Tools are
registered per-agent by passing a tools=[...] list to
V2LLMRuntime.run_json_prompt, which forwards them to the pydantic-ai
Agent. Shared tools live in git_metadata_extractor/agents/llm/agent_tools/ — add a new
module there to make a tool available to multiple agents.
Two main families:
- Per-index RAG tools (
*_rag.py) — Qdrant-backed semantic search + on-demand fetch over Infoscience, ETH Research Collection, HuggingFace, OpenAlex, ORCID, ROR, Zenodo. Plus the federated tools (search_federated_rag,lookup_entity_federated) that fan out across all nine indices in parallel. Documented inv2-rag-tools.md. - Direct provider tools — disciplines list (
list_disciplines), GitHub SPDX SBOM (query_dependencies), GitHub org / ROR org / ORCID person identity lookups, Infoscience search and orgunit, repository corpus grep, Selenium fetch (fetch_link_content_via_selenium), email hashing, UUID generation, DuckDuckGo search.
The full registered set lives in
git_metadata_extractor/agents/llm/agent_tools/__init__.py.
Tool calls emit an INFO log line from git_metadata_extractor.agents.llm.agent_tools.<module>:
INFO git_metadata_extractor.agents.llm.agent_tools.disciplines: tool call: list_disciplines — returning 46 entries
If this line is absent after an LLM repository run with
agent_runtime=llm, the model did not call the tool.
- Repository
identifiersuseschema:citation(notschema:identifier) for the DOI/citation link. TheidSourceenum values for repositories arepulse:githubRepositoryHandle,schema:citation, anduuid. - Article entities continue to use
schema:identifierfor their canonical identifier. schema:alternateNameis an internal intermediate field on organization entities: it is consumed by pipeline stages during alias matching and is not present in final/v2/extractoutputs.- Internal pipeline metadata fields whose names start with
_(e.g._person_refon Memberships) are stripped before strict validation, JSON-LD output, RDF serialisation, and any external artefact.
The most-touched knobs (full list in .env.example and CLAUDE.md):
| Variable | Default | Notes |
|---|---|---|
API_TOKEN |
unset | Bearer token guarding the protected /v2/* routes. Missing → 503 (no dev bypass). See Authentication. |
V2_RATE_LIMIT_PER_MINUTE |
unset (off) | Opt-in per-client request cap on the compute-heavy routes (POST /v2/extract + the GET extract routes). Keyed by bearer token (else IP); in-memory & per-worker. Over limit → 429 + Retry-After. See Rate limiting. |
V2_AGENT_RUNTIME_DEFAULT |
llm |
Default runtime when /v2/extract omits agent_runtime |
V2_USE_MOCK_PROVIDERS |
true |
Swap in mock GitHub/ORCID/Infoscience/ROR providers |
V2_LINK_VERACITY_ENABLED |
true |
Skip the link-veracity stage in LLM mode (rule-based skips unconditionally) |
V2_APPLY_CRITIC_PRUNING |
false |
Enable critic drop suggestions (LLM mode only) |
V2_MAX_CONCURRENT_AGENTS |
6 |
Per-stage fan-out concurrency |
V2_PROVIDER_CACHE_PATH |
.cache/v2/providers.db |
Shared provider + verdict + pipeline + job-store SQLite path |
V2_PROVIDER_CACHE_TTL_DAYS |
30 |
TTL for cached entries |
V2_PROVIDER_CACHE_ENABLED |
true |
When false, every external lookup is fresh and the async-job store is disabled |
V2_PIPELINE_CACHE_ENABLED |
true |
When false, every /extract re-runs the full pipeline |
V2_<INDEX>_RAG_ENABLED |
true |
Toggle each RAG tool family (INFOSCIENCE, ETHZ_RESEARCH_COLLECTION, HUGGINGFACE, OPENALEX, ZENODO, ORCID, ROR) |
INDEX_QDRANT_URL |
YAML-driven | Qdrant endpoint for every RAG index. Use http://gme-qdrant:6333 inside the devcontainer. |
V2_QUERY_LOG_DIR |
logs/v2_queries |
Per-request external-query log destination |
LOG_LEVEL |
INFO |
DEBUG/INFO/WARNING/ERROR |
GME_GITHUB_TOKEN |
unset | Required for healthy provider preflight |
RCP_TOKEN / OPENAI_API_KEY / OPENROUTER_API_KEY |
unset | At least one required in LLM mode |
SELENIUM_REMOTE_URL |
unset | Enables link-veracity + the fetch_link_content_via_selenium tool |
GIMIE_API_URL |
unset | Required for repository extraction. Points at the gimie-api sidecar (e.g. http://gme-gimie-api:15400); the gimie package was removed from the image. Unset → in-process gimie, which is no longer installed → RuntimeError. See operations runbook §8. |
GIMIE_API_TIMEOUT_SECONDS |
180 |
Per-request timeout for the gimie-api sidecar call. |