mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-12-03 09:53:45 +00:00
feat(openapi): generate stainless config "more" programmatically (#4164)
Generate the Stainless client config directly from code so we can validate the config before we ever write the YAML. This change enforces allowed HTTP verbs/paths, detects duplicate routes across resources, and ensures README example endpoints exist and match the OpenAPI spec. The generator now fails fast when config entries drift, keeping the published config (hopefully) more current with the spec. I think more validation can be done but this is a good start.
This commit is contained in:
parent
5fe6098350
commit
7d3db6b22c
6 changed files with 990 additions and 193 deletions
|
|
@ -11,6 +11,13 @@ This module provides functionality to generate OpenAPI specifications
|
|||
from FastAPI applications.
|
||||
"""
|
||||
|
||||
from .main import generate_openapi_spec, main
|
||||
|
||||
__all__ = ["generate_openapi_spec", "main"]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
if name in {"generate_openapi_spec", "main"}:
|
||||
from .main import generate_openapi_spec as _gos
|
||||
from .main import main as _main
|
||||
|
||||
return {"generate_openapi_spec": _gos, "main": _main}[name]
|
||||
raise AttributeError(name)
|
||||
|
|
|
|||
7
scripts/openapi_generator/stainless_config/__init__.py
Normal file
7
scripts/openapi_generator/stainless_config/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the terms described in the LICENSE file in
|
||||
# the root directory of this source tree.
|
||||
|
||||
# Package marker for Stainless config generation.
|
||||
821
scripts/openapi_generator/stainless_config/generate_config.py
Normal file
821
scripts/openapi_generator/stainless_config/generate_config.py
Normal file
|
|
@ -0,0 +1,821 @@
|
|||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the terms described in the LICENSE file in
|
||||
# the root directory of this source tree.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
HEADER = "# yaml-language-server: $schema=https://app.stainlessapi.com/config-internal.schema.json\n\n"
|
||||
|
||||
SECTION_ORDER = [
|
||||
"organization",
|
||||
"security",
|
||||
"security_schemes",
|
||||
"targets",
|
||||
"client_settings",
|
||||
"environments",
|
||||
"pagination",
|
||||
"settings",
|
||||
"openapi",
|
||||
"readme",
|
||||
"resources",
|
||||
]
|
||||
|
||||
ORGANIZATION = {
|
||||
"name": "llama-stack-client",
|
||||
"docs": "https://llama-stack.readthedocs.io/en/latest/",
|
||||
"contact": "llamastack@meta.com",
|
||||
}
|
||||
|
||||
SECURITY = [{}, {"BearerAuth": []}]
|
||||
|
||||
SECURITY_SCHEMES = {"BearerAuth": {"type": "http", "scheme": "bearer"}}
|
||||
|
||||
TARGETS = {
|
||||
"node": {
|
||||
"package_name": "llama-stack-client",
|
||||
"production_repo": "llamastack/llama-stack-client-typescript",
|
||||
"publish": {"npm": False},
|
||||
},
|
||||
"python": {
|
||||
"package_name": "llama_stack_client",
|
||||
"production_repo": "llamastack/llama-stack-client-python",
|
||||
"options": {"use_uv": True},
|
||||
"publish": {"pypi": True},
|
||||
"project_name": "llama_stack_client",
|
||||
},
|
||||
"kotlin": {
|
||||
"reverse_domain": "com.llama_stack_client.api",
|
||||
"production_repo": None,
|
||||
"publish": {"maven": False},
|
||||
},
|
||||
"go": {
|
||||
"package_name": "llama-stack-client",
|
||||
"production_repo": "llamastack/llama-stack-client-go",
|
||||
"options": {"enable_v2": True, "back_compat_use_shared_package": False},
|
||||
},
|
||||
}
|
||||
|
||||
CLIENT_SETTINGS = {
|
||||
"default_env_prefix": "LLAMA_STACK_CLIENT",
|
||||
"opts": {
|
||||
"api_key": {
|
||||
"type": "string",
|
||||
"read_env": "LLAMA_STACK_CLIENT_API_KEY",
|
||||
"auth": {"security_scheme": "BearerAuth"},
|
||||
"nullable": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
ENVIRONMENTS = {"production": "http://any-hosted-llama-stack.com"}
|
||||
|
||||
PAGINATION = [
|
||||
{
|
||||
"name": "datasets_iterrows",
|
||||
"type": "offset",
|
||||
"request": {
|
||||
"dataset_id": {"type": "string"},
|
||||
"start_index": {
|
||||
"type": "integer",
|
||||
"x-stainless-pagination-property": {"purpose": "offset_count_param"},
|
||||
},
|
||||
"limit": {"type": "integer"},
|
||||
},
|
||||
"response": {
|
||||
"data": {"type": "array", "items": {"type": "object"}},
|
||||
"next_index": {
|
||||
"type": "integer",
|
||||
"x-stainless-pagination-property": {"purpose": "offset_count_start_field"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "openai_cursor_page",
|
||||
"type": "cursor",
|
||||
"request": {
|
||||
"limit": {"type": "integer"},
|
||||
"after": {
|
||||
"type": "string",
|
||||
"x-stainless-pagination-property": {"purpose": "next_cursor_param"},
|
||||
},
|
||||
},
|
||||
"response": {
|
||||
"data": {"type": "array", "items": {}},
|
||||
"has_more": {"type": "boolean"},
|
||||
"last_id": {
|
||||
"type": "string",
|
||||
"x-stainless-pagination-property": {"purpose": "next_cursor_field"},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
SETTINGS = {
|
||||
"license": "MIT",
|
||||
"unwrap_response_fields": ["data"],
|
||||
"file_header": "Copyright (c) Meta Platforms, Inc. and affiliates.\n"
|
||||
"All rights reserved.\n"
|
||||
"\n"
|
||||
"This source code is licensed under the terms described in the "
|
||||
"LICENSE file in\n"
|
||||
"the root directory of this source tree.\n",
|
||||
}
|
||||
|
||||
OPENAPI = {
|
||||
"transformations": [
|
||||
{
|
||||
"command": "mergeObject",
|
||||
"reason": "Better return_type using enum",
|
||||
"args": {
|
||||
"target": ["$.components.schemas"],
|
||||
"object": {
|
||||
"ReturnType": {
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"array",
|
||||
"object",
|
||||
"json",
|
||||
"union",
|
||||
"chat_completion_input",
|
||||
"completion_input",
|
||||
"agent_turn_input",
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["type"],
|
||||
"type": "object",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"command": "replaceProperties",
|
||||
"reason": "Replace return type properties with better model (see above)",
|
||||
"args": {
|
||||
"filter": {
|
||||
"only": [
|
||||
"$.components.schemas.ScoringFn.properties.return_type",
|
||||
"$.components.schemas.RegisterScoringFunctionRequest.properties.return_type",
|
||||
]
|
||||
},
|
||||
"value": {"$ref": "#/components/schemas/ReturnType"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"command": "oneOfToAnyOf",
|
||||
"reason": "Prism (mock server) doesn't like one of our "
|
||||
"requests as it technically matches multiple "
|
||||
"variants",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
README = {
|
||||
"example_requests": {
|
||||
"default": {
|
||||
"type": "request",
|
||||
"endpoint": "post /v1/chat/completions",
|
||||
"params": {},
|
||||
},
|
||||
"headline": {"type": "request", "endpoint": "get /v1/models", "params": {}},
|
||||
"pagination": {
|
||||
"type": "request",
|
||||
"endpoint": "post /v1/chat/completions",
|
||||
"params": {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
ALL_RESOURCES = {
|
||||
"$shared": {
|
||||
"models": {
|
||||
"interleaved_content_item": "InterleavedContentItem",
|
||||
"interleaved_content": "InterleavedContent",
|
||||
"param_type": "ParamType",
|
||||
"safety_violation": "SafetyViolation",
|
||||
"sampling_params": "SamplingParams",
|
||||
"scoring_result": "ScoringResult",
|
||||
"system_message": "SystemMessage",
|
||||
}
|
||||
},
|
||||
"toolgroups": {
|
||||
"models": {
|
||||
"tool_group": "ToolGroup",
|
||||
"list_tool_groups_response": "ListToolGroupsResponse",
|
||||
},
|
||||
"methods": {
|
||||
"register": "post /v1/toolgroups",
|
||||
"get": "get /v1/toolgroups/{toolgroup_id}",
|
||||
"list": "get /v1/toolgroups",
|
||||
"unregister": "delete /v1/toolgroups/{toolgroup_id}",
|
||||
},
|
||||
},
|
||||
"tools": {
|
||||
"methods": {
|
||||
"get": "get /v1/tools/{tool_name}",
|
||||
"list": {"paginated": False, "endpoint": "get /v1/tools"},
|
||||
}
|
||||
},
|
||||
"tool_runtime": {
|
||||
"models": {
|
||||
"tool_def": "ToolDef",
|
||||
"tool_invocation_result": "ToolInvocationResult",
|
||||
},
|
||||
"methods": {
|
||||
"list_tools": {
|
||||
"paginated": False,
|
||||
"endpoint": "get /v1/tool-runtime/list-tools",
|
||||
},
|
||||
"invoke_tool": "post /v1/tool-runtime/invoke",
|
||||
},
|
||||
},
|
||||
"responses": {
|
||||
"models": {
|
||||
"response_object_stream": "OpenAIResponseObjectStream",
|
||||
"response_object": "OpenAIResponseObject",
|
||||
},
|
||||
"methods": {
|
||||
"create": {
|
||||
"type": "http",
|
||||
"streaming": {
|
||||
"stream_event_model": "responses.response_object_stream",
|
||||
"param_discriminator": "stream",
|
||||
},
|
||||
"endpoint": "post /v1/responses",
|
||||
},
|
||||
"retrieve": "get /v1/responses/{response_id}",
|
||||
"list": {"type": "http", "endpoint": "get /v1/responses"},
|
||||
"delete": {
|
||||
"type": "http",
|
||||
"endpoint": "delete /v1/responses/{response_id}",
|
||||
},
|
||||
},
|
||||
"subresources": {
|
||||
"input_items": {
|
||||
"methods": {
|
||||
"list": {
|
||||
"type": "http",
|
||||
"paginated": False,
|
||||
"endpoint": "get /v1/responses/{response_id}/input_items",
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"prompts": {
|
||||
"models": {"prompt": "Prompt", "list_prompts_response": "ListPromptsResponse"},
|
||||
"methods": {
|
||||
"create": "post /v1/prompts",
|
||||
"list": {"paginated": False, "endpoint": "get /v1/prompts"},
|
||||
"retrieve": "get /v1/prompts/{prompt_id}",
|
||||
"update": "post /v1/prompts/{prompt_id}",
|
||||
"delete": "delete /v1/prompts/{prompt_id}",
|
||||
"set_default_version": "post /v1/prompts/{prompt_id}/set-default-version",
|
||||
},
|
||||
"subresources": {
|
||||
"versions": {
|
||||
"methods": {
|
||||
"list": {
|
||||
"paginated": False,
|
||||
"endpoint": "get /v1/prompts/{prompt_id}/versions",
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"conversations": {
|
||||
"models": {"conversation_object": "Conversation"},
|
||||
"methods": {
|
||||
"create": {"type": "http", "endpoint": "post /v1/conversations"},
|
||||
"retrieve": "get /v1/conversations/{conversation_id}",
|
||||
"update": {
|
||||
"type": "http",
|
||||
"endpoint": "post /v1/conversations/{conversation_id}",
|
||||
},
|
||||
"delete": {
|
||||
"type": "http",
|
||||
"endpoint": "delete /v1/conversations/{conversation_id}",
|
||||
},
|
||||
},
|
||||
"subresources": {
|
||||
"items": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"type": "http",
|
||||
"endpoint": "get /v1/conversations/{conversation_id}/items/{item_id}",
|
||||
},
|
||||
"list": {
|
||||
"type": "http",
|
||||
"endpoint": "get /v1/conversations/{conversation_id}/items",
|
||||
},
|
||||
"create": {
|
||||
"type": "http",
|
||||
"endpoint": "post /v1/conversations/{conversation_id}/items",
|
||||
},
|
||||
"delete": {
|
||||
"type": "http",
|
||||
"endpoint": "delete /v1/conversations/{conversation_id}/items/{item_id}",
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"inspect": {
|
||||
"models": {
|
||||
"healthInfo": "HealthInfo",
|
||||
"providerInfo": "ProviderInfo",
|
||||
"routeInfo": "RouteInfo",
|
||||
"versionInfo": "VersionInfo",
|
||||
},
|
||||
"methods": {"health": "get /v1/health", "version": "get /v1/version"},
|
||||
},
|
||||
"embeddings": {
|
||||
"models": {"create_embeddings_response": "OpenAIEmbeddingsResponse"},
|
||||
"methods": {"create": "post /v1/embeddings"},
|
||||
},
|
||||
"chat": {
|
||||
"models": {"chat_completion_chunk": "OpenAIChatCompletionChunk"},
|
||||
"subresources": {
|
||||
"completions": {
|
||||
"methods": {
|
||||
"create": {
|
||||
"type": "http",
|
||||
"streaming": {
|
||||
"stream_event_model": "chat.chat_completion_chunk",
|
||||
"param_discriminator": "stream",
|
||||
},
|
||||
"endpoint": "post /v1/chat/completions",
|
||||
},
|
||||
"list": {
|
||||
"type": "http",
|
||||
"paginated": False,
|
||||
"endpoint": "get /v1/chat/completions",
|
||||
},
|
||||
"retrieve": {
|
||||
"type": "http",
|
||||
"endpoint": "get /v1/chat/completions/{completion_id}",
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"completions": {
|
||||
"methods": {
|
||||
"create": {
|
||||
"type": "http",
|
||||
"streaming": {"param_discriminator": "stream"},
|
||||
"endpoint": "post /v1/completions",
|
||||
}
|
||||
}
|
||||
},
|
||||
"vector_io": {
|
||||
"models": {"queryChunksResponse": "QueryChunksResponse"},
|
||||
"methods": {
|
||||
"insert": "post /v1/vector-io/insert",
|
||||
"query": "post /v1/vector-io/query",
|
||||
},
|
||||
},
|
||||
"vector_stores": {
|
||||
"models": {
|
||||
"vector_store": "VectorStoreObject",
|
||||
"list_vector_stores_response": "VectorStoreListResponse",
|
||||
"vector_store_delete_response": "VectorStoreDeleteResponse",
|
||||
"vector_store_search_response": "VectorStoreSearchResponsePage",
|
||||
},
|
||||
"methods": {
|
||||
"create": "post /v1/vector_stores",
|
||||
"list": "get /v1/vector_stores",
|
||||
"retrieve": "get /v1/vector_stores/{vector_store_id}",
|
||||
"update": "post /v1/vector_stores/{vector_store_id}",
|
||||
"delete": "delete /v1/vector_stores/{vector_store_id}",
|
||||
"search": "post /v1/vector_stores/{vector_store_id}/search",
|
||||
},
|
||||
"subresources": {
|
||||
"files": {
|
||||
"models": {"vector_store_file": "VectorStoreFileObject"},
|
||||
"methods": {
|
||||
"list": "get /v1/vector_stores/{vector_store_id}/files",
|
||||
"retrieve": "get /v1/vector_stores/{vector_store_id}/files/{file_id}",
|
||||
"update": "post /v1/vector_stores/{vector_store_id}/files/{file_id}",
|
||||
"delete": "delete /v1/vector_stores/{vector_store_id}/files/{file_id}",
|
||||
"create": "post /v1/vector_stores/{vector_store_id}/files",
|
||||
"content": "get /v1/vector_stores/{vector_store_id}/files/{file_id}/content",
|
||||
},
|
||||
},
|
||||
"file_batches": {
|
||||
"models": {
|
||||
"vector_store_file_batches": "VectorStoreFileBatchObject",
|
||||
"list_vector_store_files_in_batch_response": "VectorStoreFilesListInBatchResponse",
|
||||
},
|
||||
"methods": {
|
||||
"create": "post /v1/vector_stores/{vector_store_id}/file_batches",
|
||||
"retrieve": "get /v1/vector_stores/{vector_store_id}/file_batches/{batch_id}",
|
||||
"list_files": "get /v1/vector_stores/{vector_store_id}/file_batches/{batch_id}/files",
|
||||
"cancel": "post /v1/vector_stores/{vector_store_id}/file_batches/{batch_id}/cancel",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"models": {
|
||||
"models": {
|
||||
"model": "OpenAIModel",
|
||||
"list_models_response": "OpenAIListModelsResponse",
|
||||
},
|
||||
"methods": {
|
||||
"list": {"paginated": False, "endpoint": "get /v1/models"},
|
||||
"retrieve": "get /v1/models/{model_id}",
|
||||
"register": "post /v1/models",
|
||||
"unregister": "delete /v1/models/{model_id}",
|
||||
},
|
||||
"subresources": {"openai": {"methods": {"list": {"paginated": False, "endpoint": "get /v1/models"}}}},
|
||||
},
|
||||
"providers": {
|
||||
"models": {"list_providers_response": "ListProvidersResponse"},
|
||||
"methods": {
|
||||
"list": {"paginated": False, "endpoint": "get /v1/providers"},
|
||||
"retrieve": "get /v1/providers/{provider_id}",
|
||||
},
|
||||
},
|
||||
"routes": {
|
||||
"models": {"list_routes_response": "ListRoutesResponse"},
|
||||
"methods": {"list": {"paginated": False, "endpoint": "get /v1/inspect/routes"}},
|
||||
},
|
||||
"moderations": {
|
||||
"models": {"create_response": "ModerationObject"},
|
||||
"methods": {"create": "post /v1/moderations"},
|
||||
},
|
||||
"safety": {
|
||||
"models": {"run_shield_response": "RunShieldResponse"},
|
||||
"methods": {"run_shield": "post /v1/safety/run-shield"},
|
||||
},
|
||||
"shields": {
|
||||
"models": {"shield": "Shield", "list_shields_response": "ListShieldsResponse"},
|
||||
"methods": {
|
||||
"retrieve": "get /v1/shields/{identifier}",
|
||||
"list": {"paginated": False, "endpoint": "get /v1/shields"},
|
||||
"register": "post /v1/shields",
|
||||
"delete": "delete /v1/shields/{identifier}",
|
||||
},
|
||||
},
|
||||
"scoring": {
|
||||
"methods": {
|
||||
"score": "post /v1/scoring/score",
|
||||
"score_batch": "post /v1/scoring/score-batch",
|
||||
}
|
||||
},
|
||||
"scoring_functions": {
|
||||
"models": {
|
||||
"scoring_fn": "ScoringFn",
|
||||
"scoring_fn_params": "ScoringFnParams",
|
||||
"list_scoring_functions_response": "ListScoringFunctionsResponse",
|
||||
},
|
||||
"methods": {
|
||||
"retrieve": "get /v1/scoring-functions/{scoring_fn_id}",
|
||||
"list": {"paginated": False, "endpoint": "get /v1/scoring-functions"},
|
||||
"register": "post /v1/scoring-functions",
|
||||
"unregister": "delete /v1/scoring-functions/{scoring_fn_id}",
|
||||
},
|
||||
},
|
||||
"files": {
|
||||
"models": {
|
||||
"file": "OpenAIFileObject",
|
||||
"list_files_response": "ListOpenAIFileResponse",
|
||||
"delete_file_response": "OpenAIFileDeleteResponse",
|
||||
},
|
||||
"methods": {
|
||||
"create": "post /v1/files",
|
||||
"list": "get /v1/files",
|
||||
"retrieve": "get /v1/files/{file_id}",
|
||||
"delete": "delete /v1/files/{file_id}",
|
||||
"content": "get /v1/files/{file_id}/content",
|
||||
},
|
||||
},
|
||||
"batches": {
|
||||
"methods": {
|
||||
"create": "post /v1/batches",
|
||||
"list": "get /v1/batches",
|
||||
"retrieve": "get /v1/batches/{batch_id}",
|
||||
"cancel": "post /v1/batches/{batch_id}/cancel",
|
||||
}
|
||||
},
|
||||
"alpha": {
|
||||
"subresources": {
|
||||
"inference": {"methods": {"rerank": "post /v1alpha/inference/rerank"}},
|
||||
"post_training": {
|
||||
"models": {
|
||||
"algorithm_config": "AlgorithmConfig",
|
||||
"post_training_job": "PostTrainingJob",
|
||||
"list_post_training_jobs_response": "ListPostTrainingJobsResponse",
|
||||
},
|
||||
"methods": {
|
||||
"preference_optimize": "post /v1alpha/post-training/preference-optimize",
|
||||
"supervised_fine_tune": "post /v1alpha/post-training/supervised-fine-tune",
|
||||
},
|
||||
"subresources": {
|
||||
"job": {
|
||||
"methods": {
|
||||
"artifacts": "get /v1alpha/post-training/job/artifacts",
|
||||
"cancel": "post /v1alpha/post-training/job/cancel",
|
||||
"status": "get /v1alpha/post-training/job/status",
|
||||
"list": {
|
||||
"paginated": False,
|
||||
"endpoint": "get /v1alpha/post-training/jobs",
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"benchmarks": {
|
||||
"models": {
|
||||
"benchmark": "Benchmark",
|
||||
"list_benchmarks_response": "ListBenchmarksResponse",
|
||||
},
|
||||
"methods": {
|
||||
"retrieve": "get /v1alpha/eval/benchmarks/{benchmark_id}",
|
||||
"list": {
|
||||
"paginated": False,
|
||||
"endpoint": "get /v1alpha/eval/benchmarks",
|
||||
},
|
||||
"register": "post /v1alpha/eval/benchmarks",
|
||||
"unregister": "delete /v1alpha/eval/benchmarks/{benchmark_id}",
|
||||
},
|
||||
},
|
||||
"eval": {
|
||||
"models": {
|
||||
"evaluate_response": "EvaluateResponse",
|
||||
"benchmark_config": "BenchmarkConfig",
|
||||
"job": "Job",
|
||||
},
|
||||
"methods": {
|
||||
"evaluate_rows": "post /v1alpha/eval/benchmarks/{benchmark_id}/evaluations",
|
||||
"run_eval": "post /v1alpha/eval/benchmarks/{benchmark_id}/jobs",
|
||||
"evaluate_rows_alpha": "post /v1alpha/eval/benchmarks/{benchmark_id}/evaluations",
|
||||
"run_eval_alpha": "post /v1alpha/eval/benchmarks/{benchmark_id}/jobs",
|
||||
},
|
||||
"subresources": {
|
||||
"jobs": {
|
||||
"methods": {
|
||||
"cancel": "delete /v1alpha/eval/benchmarks/{benchmark_id}/jobs/{job_id}",
|
||||
"status": "get /v1alpha/eval/benchmarks/{benchmark_id}/jobs/{job_id}",
|
||||
"retrieve": "get /v1alpha/eval/benchmarks/{benchmark_id}/jobs/{job_id}/result",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"beta": {
|
||||
"subresources": {
|
||||
"datasets": {
|
||||
"models": {"list_datasets_response": "ListDatasetsResponse"},
|
||||
"methods": {
|
||||
"register": "post /v1beta/datasets",
|
||||
"retrieve": "get /v1beta/datasets/{dataset_id}",
|
||||
"list": {"paginated": False, "endpoint": "get /v1beta/datasets"},
|
||||
"unregister": "delete /v1beta/datasets/{dataset_id}",
|
||||
"iterrows": "get /v1beta/datasetio/iterrows/{dataset_id}",
|
||||
"appendrows": "post /v1beta/datasetio/append-rows/{dataset_id}",
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
HTTP_METHODS = {"get", "post", "put", "patch", "delete", "options", "head"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Endpoint:
|
||||
method: str
|
||||
path: str
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, value: Any) -> Endpoint:
|
||||
if isinstance(value, str):
|
||||
method, _, path = value.partition(" ")
|
||||
return cls._from_parts(method, path)
|
||||
if isinstance(value, dict) and "endpoint" in value:
|
||||
method, _, path = value["endpoint"].partition(" ")
|
||||
extra = {k: v for k, v in value.items() if k != "endpoint"}
|
||||
endpoint = cls._from_parts(method, path)
|
||||
endpoint.extra.update(extra)
|
||||
return endpoint
|
||||
raise ValueError(f"Unsupported endpoint value: {value!r}")
|
||||
|
||||
@classmethod
|
||||
def _from_parts(cls, method: str, path: str) -> Endpoint:
|
||||
method = method.strip().lower()
|
||||
path = path.strip()
|
||||
if method not in HTTP_METHODS:
|
||||
raise ValueError(f"Unsupported HTTP method for Stainless config: {method!r}")
|
||||
if not path.startswith("/"):
|
||||
raise ValueError(f"Endpoint path must start with '/': {path!r}")
|
||||
return cls(method=method, path=path)
|
||||
|
||||
def to_config(self) -> Any:
|
||||
if not self.extra:
|
||||
return f"{self.method} {self.path}"
|
||||
data = dict(self.extra)
|
||||
data["endpoint"] = f"{self.method} {self.path}"
|
||||
return data
|
||||
|
||||
def route_key(self) -> str:
|
||||
return f"{self.method} {self.path}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Resource:
|
||||
models: dict[str, str] | None = None
|
||||
methods: dict[str, Endpoint] = field(default_factory=dict)
|
||||
subresources: dict[str, Resource] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> Resource:
|
||||
models = data.get("models")
|
||||
methods = {name: Endpoint.from_config(value) for name, value in data.get("methods", {}).items()}
|
||||
subresources = {name: cls.from_dict(value) for name, value in data.get("subresources", {}).items()}
|
||||
return cls(models=models, methods=methods, subresources=subresources)
|
||||
|
||||
def to_config(self) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {}
|
||||
if self.models:
|
||||
result["models"] = self.models
|
||||
if self.methods:
|
||||
result["methods"] = {name: endpoint.to_config() for name, endpoint in self.methods.items()}
|
||||
if self.subresources:
|
||||
result["subresources"] = {name: resource.to_config() for name, resource in self.subresources.items()}
|
||||
return result
|
||||
|
||||
def collect_endpoint_paths(self) -> set[str]:
|
||||
paths = {endpoint.route_key() for endpoint in self.methods.values()}
|
||||
for subresource in self.subresources.values():
|
||||
paths.update(subresource.collect_endpoint_paths())
|
||||
return paths
|
||||
|
||||
def iter_endpoints(self, prefix: str) -> Iterator[tuple[str, str]]:
|
||||
for method_name, endpoint in self.methods.items():
|
||||
label = f"{prefix}.{method_name}" if prefix else method_name
|
||||
yield endpoint.route_key(), label
|
||||
for sub_name, subresource in self.subresources.items():
|
||||
sub_prefix = f"{prefix}.{sub_name}" if prefix else sub_name
|
||||
yield from subresource.iter_endpoints(sub_prefix)
|
||||
|
||||
|
||||
_RESOURCES = {name: Resource.from_dict(data) for name, data in ALL_RESOURCES.items()}
|
||||
|
||||
|
||||
def _load_openapi_paths(openapi_path: Path) -> set[str]:
|
||||
spec = yaml.safe_load(openapi_path.read_text()) or {}
|
||||
paths: set[str] = set()
|
||||
for path, path_item in (spec.get("paths") or {}).items():
|
||||
if not isinstance(path_item, dict):
|
||||
continue
|
||||
for method, operation in path_item.items():
|
||||
if not isinstance(operation, dict):
|
||||
continue
|
||||
paths.add(f"{str(method).lower()} {path}")
|
||||
return paths
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StainlessConfig:
|
||||
organization: dict[str, Any]
|
||||
security: list[Any]
|
||||
security_schemes: dict[str, Any]
|
||||
targets: dict[str, Any]
|
||||
client_settings: dict[str, Any]
|
||||
environments: dict[str, Any]
|
||||
pagination: list[dict[str, Any]]
|
||||
settings: dict[str, Any]
|
||||
openapi: dict[str, Any]
|
||||
readme: dict[str, Any]
|
||||
resources: dict[str, Resource]
|
||||
|
||||
@classmethod
|
||||
def make(cls) -> StainlessConfig:
|
||||
return cls(
|
||||
organization=ORGANIZATION,
|
||||
security=SECURITY,
|
||||
security_schemes=SECURITY_SCHEMES,
|
||||
targets=TARGETS,
|
||||
client_settings=CLIENT_SETTINGS,
|
||||
environments=ENVIRONMENTS,
|
||||
pagination=PAGINATION,
|
||||
settings=SETTINGS,
|
||||
openapi=OPENAPI,
|
||||
readme=README,
|
||||
resources=dict(_RESOURCES),
|
||||
)
|
||||
|
||||
def referenced_paths(self) -> set[str]:
|
||||
paths: set[str] = set()
|
||||
for resource in self.resources.values():
|
||||
paths.update(resource.collect_endpoint_paths())
|
||||
paths.update(self.readme_endpoint_paths())
|
||||
return paths
|
||||
|
||||
def readme_endpoint_paths(self) -> set[str]:
|
||||
example_requests = self.readme.get("example_requests", {}) if self.readme else {}
|
||||
paths: set[str] = set()
|
||||
for entry in example_requests.values():
|
||||
endpoint = entry.get("endpoint") if isinstance(entry, dict) else None
|
||||
if isinstance(endpoint, str):
|
||||
method, _, route = endpoint.partition(" ")
|
||||
method = method.strip().lower()
|
||||
route = route.strip()
|
||||
if method and route:
|
||||
paths.add(f"{method} {route}")
|
||||
return paths
|
||||
|
||||
def endpoint_map(self) -> dict[str, list[str]]:
|
||||
mapping: dict[str, list[str]] = {}
|
||||
for resource_name, resource in self.resources.items():
|
||||
for route, label in resource.iter_endpoints(resource_name):
|
||||
mapping.setdefault(route, []).append(label)
|
||||
return mapping
|
||||
|
||||
def validate_unique_endpoints(self) -> None:
|
||||
duplicates: dict[str, list[str]] = {}
|
||||
for route, labels in self.endpoint_map().items():
|
||||
top_levels = {label.split(".", 1)[0] for label in labels}
|
||||
if len(top_levels) > 1:
|
||||
duplicates[route] = labels
|
||||
if duplicates:
|
||||
formatted = "\n".join(
|
||||
f" - {route} defined in: {', '.join(sorted(labels))}" for route, labels in sorted(duplicates.items())
|
||||
)
|
||||
raise ValueError("Duplicate endpoints found across resources:\n" + formatted)
|
||||
|
||||
def validate_readme_endpoints(self) -> None:
|
||||
resource_paths: set[str] = set()
|
||||
for resource in self.resources.values():
|
||||
resource_paths.update(resource.collect_endpoint_paths())
|
||||
missing = sorted(path for path in self.readme_endpoint_paths() if path not in resource_paths)
|
||||
if missing:
|
||||
formatted = "\n".join(f" - {path}" for path in missing)
|
||||
raise ValueError("README example endpoints are not present in Stainless resources:\n" + formatted)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
cfg: dict[str, Any] = {}
|
||||
for section in SECTION_ORDER:
|
||||
if section == "resources":
|
||||
cfg[section] = {name: resource.to_config() for name, resource in self.resources.items()}
|
||||
continue
|
||||
cfg[section] = getattr(self, section)
|
||||
return cfg
|
||||
|
||||
def validate_against_openapi(self, openapi_path: Path) -> None:
|
||||
if not openapi_path.exists():
|
||||
raise FileNotFoundError(f"OpenAPI spec not found at {openapi_path}")
|
||||
spec_paths = _load_openapi_paths(openapi_path)
|
||||
config_paths = self.referenced_paths()
|
||||
missing = sorted(path for path in config_paths if path not in spec_paths)
|
||||
if missing:
|
||||
formatted = "\n".join(f" - {path}" for path in missing)
|
||||
raise ValueError("Stainless config references missing endpoints:\n" + formatted)
|
||||
|
||||
def validate(self, openapi_path: Path | None = None) -> None:
|
||||
self.validate_unique_endpoints()
|
||||
self.validate_readme_endpoints()
|
||||
if openapi_path is not None:
|
||||
self.validate_against_openapi(openapi_path)
|
||||
|
||||
|
||||
def build_config() -> dict[str, Any]:
|
||||
return StainlessConfig.make().to_dict()
|
||||
|
||||
|
||||
def write_config(repo_root: Path, openapi_path: Path | None = None) -> Path:
|
||||
stainless_config = StainlessConfig.make()
|
||||
spec_path = (openapi_path or (repo_root / "client-sdks" / "stainless" / "openapi.yml")).resolve()
|
||||
stainless_config.validate(spec_path)
|
||||
yaml_text = yaml.safe_dump(stainless_config.to_dict(), sort_keys=False)
|
||||
output = repo_root / "client-sdks" / "stainless" / "config.yml"
|
||||
output.write_text(HEADER + yaml_text)
|
||||
return output
|
||||
|
||||
|
||||
def main() -> None:
|
||||
repo_root = Path(__file__).resolve().parents[3]
|
||||
output = write_config(repo_root)
|
||||
print(f"Wrote Stainless config: {output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -17,3 +17,5 @@ PYTHONPATH=$PYTHONPATH:$stack_dir \
|
|||
python3 -m scripts.openapi_generator "$stack_dir"/docs/static
|
||||
|
||||
cp "$stack_dir"/docs/static/stainless-llama-stack-spec.yaml "$stack_dir"/client-sdks/stainless/openapi.yml
|
||||
PYTHONPATH=$PYTHONPATH:$stack_dir \
|
||||
python3 -m scripts.openapi_generator.stainless_config.generate_config
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue