From 86ac0f7745cc9a0fe51e6689db121d070c9125d4 Mon Sep 17 00:00:00 2001 From: Calcium-Ion Date: Mon, 27 Jul 2026 15:56:21 +0800 Subject: [PATCH] refactor: extract protocol conversion layer into standalone relaykit module (#6369) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(relayconvert): add golden snapshot matrix and relaykit boundary guard Phase 0 of the relaykit extraction plan: pin byte-level output of every registered (from,to) request/response/stream conversion route, and forbid kit-bound packages from growing host-only imports. * wip(relayconvert): drop gin.Context from converter signatures; add convmeta draft Phase 1 in progress: relayconvert now takes context.Context; host media resolver adapts gin.Context back at the service boundary. * refactor(relayconvert): decouple converters from RelayInfo, gin, and settings Phase 1 of the relaykit extraction plan: - converters now depend on convmeta.Meta (implemented by RelayInfo) instead of *relaycommon.RelayInfo; ClaudeConvertInfo and the format guesser move to convmeta with aliases left behind - host settings reach converters via a convmeta.Options snapshot built in RelayInfo.ConvOptions; no more model_setting/reasoning global reads inside the conversion layer - effort-suffix helpers move to service/relayconvert/reasoning (old package forwards); chat-to-responses upgrade policy moves to service (host routing logic, not conversion) - golden conversion matrix unchanged * test(relayconvert): tighten boundary — kit packages now free of gin/setting imports * refactor(dto): drop gin and logger dependencies Phase 2 (part 1): dto.Request.IsStream now takes *http.Request instead of *gin.Context (Gemini's impl reads query/path off the std request); dto's three logger calls become common.SysError. Boundary test allowlist is now empty — kit-bound packages import no gin/setting/logger/model. * refactor(kit): extract dependency-free kitutil; dto/types/relayconvert stop importing common Phase 2 of the relaykit extraction plan: - new service/relayconvert/kitutil holds the pure helpers the kit needs (JSON wrappers, pointer/string/uuid/timestamp utils, MaskSensitiveInfo, pluggable LogInfo/LogError hooks, Debug flag) - dto, types, and all relayconvert packages now use kitutil; their only remaining internal deps are dto/types/constant - common keeps every original symbol (MaskSensitiveInfo delegates to kitutil) so host code is untouched; main.go routes kit logging into common.SysLog/SysError and mirrors DebugEnabled - golden conversion matrix unchanged * refactor(kit): move EndpointType/FinishReason to types; OpenRouter dialect via Options Kit packages (dto/types/relayconvert/reasonmap) no longer import constant: - EndpointType and finish-reason values live in types; constant re-exports - the OpenRouter special-case in claude->openai request conversion reads Options.OpenRouterDialect, set by the host from the channel type; InitChannelMeta invalidates the cached snapshot on channel switch * refactor: extract relaykit submodule (dto/types/relayconvert/reasonmap) Phase 3 of the relaykit extraction plan: - new go module github.com/QuantumNous/new-api/relaykit containing dto (minus task family), types, relayconvert (with convmeta/kitutil/reasoning), and reasonmap; host consumes it via require + replace, go.work for dev - task-family dto (task/suno/midjourney/video) stays in the host dto package; dual-consumer host files alias it as taskdto - relaykit builds and tests standalone (GOWORK=off): no host imports, no gin, no DB, no settings - golden conversion matrix unchanged * build(docker): copy relaykit/go.mod before go mod download The local-replace submodule's go.mod must exist inside the build context for the main module graph to resolve. * fix: address relaykit extraction regressions * fix: address relaykit review regressions * docs: document Meta nil receiver contract * fix(relaykit): fail OpenAI→Claude conversion without max_tokens; reject negative default_max_tokens The Claude Messages API requires max_tokens (omitting it is a 400 "Field required"), but with a nil Options.Claude.DefaultMaxTokens hook the converters silently emitted a request the upstream is guaranteed to reject. Both OpenAI Chat and Responses → Claude conversions now return sharedclaude.ErrMissingMaxTokens when no path (client value, default hook, thinking-adapter floor) supplied one. Unreachable in the host, which always configures the hook. Host side, claude.default_max_tokens now rejects negative values at the option API before persisting — they would wrap into huge unsigned values during conversion. Zero stays allowed: the current API treats max_tokens: 0 as cache pre-warming. * fix: make Gemini safety settings read path race-free --- .gitignore | 2 + Dockerfile | 5 +- common/str.go | 130 +---- constant/endpoint_type.go | 30 +- constant/finish_reason.go | 13 +- constant/waffo_pay_method.go | 4 +- controller/billing.go | 2 +- controller/channel-billing.go | 2 +- controller/channel-test.go | 4 +- controller/channel.go | 2 +- controller/channel_test_internal_test.go | 4 +- controller/channel_upstream_update.go | 4 +- controller/channel_upstream_update_test.go | 2 +- controller/model.go | 4 +- controller/model_list_test.go | 2 +- controller/option.go | 19 + controller/option_claude_test.go | 34 ++ controller/option_gemini_test.go | 34 ++ controller/playground.go | 2 +- controller/ratio_sync.go | 2 +- controller/relay.go | 17 +- controller/task.go | 2 +- controller/user.go | 2 +- go.mod | 8 +- go.sum | 8 +- i18n/i18n.go | 2 +- main.go | 7 + makefile | 11 +- middleware/auth.go | 2 +- middleware/distributor.go | 7 +- middleware/i18n.go | 2 +- middleware/performance.go | 2 +- middleware/utils.go | 2 +- model/ability.go | 2 +- model/channel.go | 4 +- model/channel_cache.go | 2 +- model/channel_settings_test.go | 2 +- model/log.go | 2 +- model/pricing.go | 4 +- model/pricing_endpoint_test.go | 2 +- model/task.go | 2 +- model/user.go | 2 +- model/user_cache.go | 2 +- model/user_update_test.go | 2 +- pkg/billingexpr/compile.go | 48 +- pkg/billingexpr/run.go | 26 +- relay/alpha_search_handler.go | 4 +- relay/audio_handler.go | 4 +- relay/channel/adapter.go | 9 +- relay/channel/advancedcustom/adaptor.go | 6 +- relay/channel/advancedcustom/adaptor_test.go | 6 +- relay/channel/ali/adaptor.go | 4 +- relay/channel/ali/dto.go | 2 +- relay/channel/ali/image.go | 4 +- relay/channel/ali/image_wan.go | 2 +- relay/channel/ali/rerank.go | 4 +- relay/channel/ali/text.go | 2 +- relay/channel/api_request.go | 2 +- relay/channel/aws/adaptor.go | 4 +- relay/channel/aws/dto.go | 2 +- relay/channel/aws/relay-aws.go | 4 +- relay/channel/baidu/adaptor.go | 4 +- relay/channel/baidu/dto.go | 2 +- relay/channel/baidu/relay-baidu.go | 4 +- relay/channel/baidu_v2/adaptor.go | 4 +- relay/channel/claude/adaptor.go | 6 +- .../claude/message_delta_usage_patch_test.go | 2 +- relay/channel/claude/relay-claude.go | 6 +- relay/channel/claude/relay_claude_test.go | 9 +- relay/channel/claude/tool_billing_test.go | 4 +- relay/channel/cloudflare/adaptor.go | 4 +- relay/channel/cloudflare/dto.go | 2 +- relay/channel/cloudflare/relay_cloudflare.go | 4 +- relay/channel/codex/adaptor.go | 4 +- relay/channel/cohere/adaptor.go | 4 +- relay/channel/cohere/dto.go | 2 +- relay/channel/cohere/relay-cohere.go | 4 +- relay/channel/coze/adaptor.go | 4 +- relay/channel/coze/relay-coze.go | 4 +- relay/channel/deepseek/adaptor.go | 4 +- relay/channel/dify/adaptor.go | 4 +- relay/channel/dify/dto.go | 2 +- relay/channel/dify/relay-dify.go | 4 +- relay/channel/gemini/adaptor.go | 6 +- .../channel/gemini/adaptor_responses_test.go | 2 +- relay/channel/gemini/relay-gemini-native.go | 4 +- relay/channel/gemini/relay-gemini.go | 6 +- .../channel/gemini/relay_gemini_usage_test.go | 4 +- relay/channel/gemini/relay_responses.go | 6 +- relay/channel/gemini/relay_responses_test.go | 4 +- relay/channel/jimeng/adaptor.go | 4 +- relay/channel/jimeng/image.go | 4 +- relay/channel/jina/adaptor.go | 4 +- relay/channel/minimax/adaptor.go | 4 +- relay/channel/minimax/adaptor_test.go | 2 +- relay/channel/minimax/image.go | 4 +- relay/channel/minimax/relay-minimax.go | 2 +- relay/channel/minimax/tts.go | 4 +- relay/channel/mistral/adaptor.go | 4 +- relay/channel/mistral/text.go | 2 +- relay/channel/mokaai/adaptor.go | 4 +- relay/channel/mokaai/relay-mokaai.go | 4 +- relay/channel/moonshot/adaptor.go | 4 +- relay/channel/moonshot/adaptor_test.go | 2 +- relay/channel/ollama/adaptor.go | 4 +- relay/channel/ollama/relay-ollama.go | 4 +- relay/channel/ollama/stream.go | 4 +- relay/channel/ollama/stream_test.go | 2 +- relay/channel/openai/adaptor.go | 4 +- relay/channel/openai/audio.go | 4 +- relay/channel/openai/chat_via_responses.go | 6 +- .../channel/openai/chat_via_responses_test.go | 2 +- relay/channel/openai/helper.go | 6 +- relay/channel/openai/image_edit_test.go | 2 +- relay/channel/openai/relay-openai.go | 6 +- relay/channel/openai/relay_image.go | 4 +- relay/channel/openai/relay_realtime.go | 4 +- relay/channel/openai/relay_responses.go | 4 +- .../openai/relay_responses_billing_test.go | 2 +- .../channel/openai/relay_responses_compact.go | 4 +- relay/channel/openai/responses_via_chat.go | 6 +- relay/channel/openai/usage.go | 2 +- relay/channel/palm/adaptor.go | 4 +- relay/channel/palm/dto.go | 2 +- relay/channel/palm/relay-palm.go | 4 +- relay/channel/perplexity/adaptor.go | 4 +- relay/channel/perplexity/relay-perplexity.go | 2 +- relay/channel/replicate/adaptor.go | 4 +- relay/channel/siliconflow/adaptor.go | 4 +- relay/channel/siliconflow/dto.go | 2 +- .../channel/siliconflow/relay-siliconflow.go | 4 +- relay/channel/submodel/adaptor.go | 4 +- relay/channel/task/ali/adaptor.go | 7 +- relay/channel/task/doubao/adaptor.go | 7 +- relay/channel/task/gemini/adaptor.go | 7 +- relay/channel/task/hailuo/adaptor.go | 7 +- relay/channel/task/jimeng/adaptor.go | 7 +- relay/channel/task/kling/adaptor.go | 7 +- relay/channel/task/vertex/adaptor.go | 7 +- relay/channel/task/vidu/adaptor.go | 7 +- relay/channel/tencent/adaptor.go | 4 +- relay/channel/tencent/relay-tencent.go | 4 +- relay/channel/vertex/adaptor.go | 4 +- relay/channel/vertex/dto.go | 2 +- relay/channel/volcengine/adaptor.go | 4 +- relay/channel/volcengine/tts.go | 4 +- relay/channel/xai/adaptor.go | 4 +- relay/channel/xai/dto.go | 2 +- relay/channel/xai/text.go | 4 +- relay/channel/xunfei/adaptor.go | 4 +- relay/channel/xunfei/dto.go | 2 +- relay/channel/xunfei/relay-xunfei.go | 4 +- relay/channel/zhipu/adaptor.go | 4 +- relay/channel/zhipu/dto.go | 2 +- relay/channel/zhipu/relay-zhipu.go | 4 +- relay/channel/zhipu_4v/adaptor.go | 4 +- relay/channel/zhipu_4v/dto.go | 4 +- relay/channel/zhipu_4v/image.go | 4 +- relay/channel/zhipu_4v/relay-zhipu_v4.go | 2 +- relay/chat_completions_via_responses.go | 4 +- relay/chat_completions_via_responses_test.go | 2 +- relay/claude_handler.go | 4 +- relay/common/override.go | 2 +- relay/common/override_test.go | 4 +- relay/common/relay_info.go | 155 +++++- .../common/relay_info_gemini_settings_test.go | 25 + relay/common/relay_info_test.go | 42 +- relay/common/request_conversion.go | 27 +- relay/common/stream_status.go | 6 +- relay/common/tool_usage.go | 2 +- relay/common/tool_usage_test.go | 2 +- relay/common_handler/rerank.go | 4 +- relay/compatible_handler.go | 4 +- relay/embedding_handler.go | 4 +- relay/gemini_handler.go | 6 +- relay/helper/billing_expr_request.go | 2 +- relay/helper/billing_expr_request_test.go | 2 +- relay/helper/common.go | 4 +- relay/helper/model_mapped.go | 2 +- relay/helper/openai_image_request_test.go | 4 +- relay/helper/price.go | 2 +- relay/helper/price_test.go | 2 +- relay/helper/valid_request.go | 4 +- relay/image_handler.go | 4 +- relay/param_override_error.go | 2 +- relay/rerank_handler.go | 4 +- relay/responses_handler.go | 4 +- relay/websocket.go | 4 +- {dto => relaykit/dto}/alpha_search_request.go | 7 +- {dto => relaykit/dto}/audio.go | 7 +- {dto => relaykit/dto}/billing_usage.go | 0 {dto => relaykit/dto}/billing_usage_test.go | 4 +- {dto => relaykit/dto}/channel_settings.go | 28 +- .../dto}/channel_settings_test.go | 24 +- {dto => relaykit/dto}/claude.go | 27 +- {dto => relaykit/dto}/embedding.go | 7 +- {dto => relaykit/dto}/error.go | 12 +- {dto => relaykit/dto}/gemini.go | 45 +- .../dto}/gemini_generation_config_test.go | 14 +- {dto => relaykit/dto}/gemini_isstream_test.go | 11 +- {dto => relaykit/dto}/gemini_response_test.go | 10 +- {dto => relaykit/dto}/notify.go | 0 {dto => relaykit/dto}/openai_compaction.go | 2 +- {dto => relaykit/dto}/openai_image.go | 19 +- {dto => relaykit/dto}/openai_request.go | 53 +- .../dto}/openai_request_zero_value_test.go | 10 +- {dto => relaykit/dto}/openai_response.go | 6 +- .../openai_responses_compaction_request.go | 7 +- {dto => relaykit/dto}/openai_video.go | 0 {dto => relaykit/dto}/playground.go | 0 {dto => relaykit/dto}/pricing.go | 12 +- {dto => relaykit/dto}/ratio_sync.go | 0 {dto => relaykit/dto}/realtime.go | 2 +- {dto => relaykit/dto}/request_common.go | 8 +- {dto => relaykit/dto}/rerank.go | 6 +- {dto => relaykit/dto}/sensitive.go | 0 {dto => relaykit/dto}/user_settings.go | 0 {dto => relaykit/dto}/values.go | 0 relaykit/go.mod | 24 + relaykit/go.sum | 40 ++ {relay => relaykit}/reasonmap/reasonmap.go | 6 +- relaykit/relayconvert/boundary_test.go | 100 ++++ .../claude_default_max_tokens_test.go | 111 ++++ relaykit/relayconvert/convmeta/format.go | 31 ++ relaykit/relayconvert/convmeta/meta.go | 215 ++++++++ relaykit/relayconvert/convmeta/meta_test.go | 38 ++ relaykit/relayconvert/convmeta/options.go | 79 +++ relaykit/relayconvert/golden_test.go | 475 ++++++++++++++++++ .../claude_messages/to_oai_chat_req.go | 34 +- .../claude_messages/to_oai_chat_resp.go | 18 +- .../internal/gemini_chat/to_oai_chat_req.go | 29 +- .../internal/gemini_chat/to_oai_chat_resp.go | 34 +- .../internal/jsonutil/stringify.go | 4 +- .../relayconvert/internal/media/media.go | 8 +- .../oai_chat/to_claude_messages_req.go | 72 +-- .../oai_chat/to_claude_messages_resp.go | 146 +++--- .../oai_chat/to_claude_messages_resp_test.go | 10 +- .../internal/oai_chat/to_gemini_chat_req.go | 61 +-- .../internal/oai_chat/to_gemini_chat_resp.go | 14 +- .../oai_chat/to_gemini_chat_resp_test.go | 6 +- .../internal/oai_chat/to_oai_responses_req.go | 44 +- .../oai_chat/to_oai_responses_req_test.go | 2 +- .../oai_chat/to_oai_responses_resp.go | 10 +- .../oai_chat/to_oai_responses_resp_test.go | 2 +- .../oai_chat/to_oai_responses_stream_resp.go | 2 +- .../internal/oai_responses/req_helpers.go | 46 +- .../oai_responses/to_claude_messages_req.go | 62 +-- .../oai_responses/to_gemini_chat_req.go | 59 ++- .../to_gemini_chat_req_preprocess.go | 28 +- .../internal/oai_responses/to_oai_chat_req.go | 84 ++-- .../oai_responses/to_oai_chat_req_test.go | 6 +- .../oai_responses/to_oai_chat_resp.go | 6 +- .../oai_responses/to_oai_chat_resp_test.go | 2 +- .../oai_responses/to_oai_chat_stream_resp.go | 8 +- .../internal/shared/claude/cache.go | 0 .../internal/shared/claude/errors.go | 10 + .../internal/shared/claude/tool_choice.go | 2 +- .../internal/shared/gemini/request.go | 45 +- .../internal/shared/gemini/schema.go | 2 +- relaykit/relayconvert/kitutil/json.go | 86 ++++ relaykit/relayconvert/kitutil/log.go | 65 +++ relaykit/relayconvert/kitutil/log_test.go | 31 ++ relaykit/relayconvert/kitutil/mask.go | 134 +++++ relaykit/relayconvert/kitutil/value.go | 52 ++ {service => relaykit}/relayconvert/media.go | 2 +- relaykit/relayconvert/reasoning/suffix.go | 51 ++ relaykit/relayconvert/request_compat.go | 48 ++ .../relayconvert/request_registry.go | 52 +- .../relayconvert/request_registry_test.go | 154 +++--- .../relayconvert/response_compat.go | 20 +- .../relayconvert/response_registry.go | 96 ++-- .../relayconvert/response_registry_test.go | 28 +- relaykit/relayconvert/safety_settings_test.go | 94 ++++ .../request/claude_to_gemini.golden.json | 94 ++++ .../request/claude_to_openai.golden.json | 67 +++ .../claude_to_openai_responses.golden.json | 54 ++ .../request/gemini_to_claude.golden.json | 61 +++ .../request/gemini_to_openai.golden.json | 68 +++ .../gemini_to_openai_responses.golden.json | 55 ++ .../openai_responses_to_claude.golden.json | 70 +++ .../openai_responses_to_gemini.golden.json | 94 ++++ .../openai_responses_to_openai.golden.json | 63 +++ .../request/openai_to_claude.golden.json | 81 +++ .../request/openai_to_gemini.golden.json | 107 ++++ .../openai_to_openai_responses.golden.json | 59 +++ .../response/claude_to_gemini.golden.json | 66 +++ .../response/claude_to_openai.golden.json | 64 +++ .../claude_to_openai_responses.golden.json | 92 ++++ .../response/gemini_to_claude.golden.json | 55 ++ .../response/gemini_to_openai.golden.json | 63 +++ .../gemini_to_openai_responses.golden.json | 91 ++++ .../openai_responses_to_claude.golden.json | 55 ++ .../openai_responses_to_gemini.golden.json | 62 +++ .../openai_responses_to_openai.golden.json | 74 +++ .../response/openai_to_claude.golden.json | 55 ++ .../response/openai_to_gemini.golden.json | 62 +++ .../openai_to_openai_responses.golden.json | 117 +++++ .../stream/claude_to_gemini.golden.json | 120 +++++ .../stream/claude_to_openai.golden.json | 181 +++++++ .../claude_to_openai_responses.golden.json | 204 ++++++++ .../stream/gemini_to_claude.golden.json | 88 ++++ .../stream/gemini_to_openai.golden.json | 134 +++++ .../gemini_to_openai_responses.golden.json | 226 +++++++++ .../openai_responses_to_claude.golden.json | 94 ++++ .../openai_responses_to_gemini.golden.json | 132 +++++ .../openai_responses_to_openai.golden.json | 123 +++++ .../stream/openai_to_claude.golden.json | 141 ++++++ .../stream/openai_to_gemini.golden.json | 158 ++++++ .../openai_to_openai_responses.golden.json | 238 +++++++++ .../relayconvert/text_converter_registry.go | 2 +- .../text_converter_registry_test.go | 2 +- {types => relaykit/types}/channel_error.go | 0 relaykit/types/endpoint_type.go | 30 ++ {types => relaykit/types}/error.go | 12 +- {types => relaykit/types}/file_data.go | 0 {types => relaykit/types}/file_source.go | 0 {types => relaykit/types}/price_data.go | 0 {types => relaykit/types}/relay_format.go | 0 {types => relaykit/types}/request_meta.go | 0 {types => relaykit/types}/rw_map.go | 10 +- {types => relaykit/types}/set.go | 0 router/relay-router.go | 2 +- service/billing.go | 2 +- service/billing_session.go | 2 +- service/billing_usage.go | 2 +- service/channel.go | 4 +- service/channel_affinity.go | 4 +- service/channel_affinity_usage_cache_test.go | 4 +- service/convert.go | 4 +- service/convert_test.go | 47 +- service/error.go | 23 +- service/error_test.go | 2 +- service/file_decoder.go | 2 +- service/file_service.go | 2 +- service/log_info_generate.go | 4 +- service/openai_chat_responses_compat.go | 4 +- service/openai_chat_responses_mode.go | 46 +- service/quota.go | 4 +- service/quota_saturation_test.go | 2 +- .../relayconvert/internal/matcher/regex.go | 33 -- .../relayconvert/internal/meta/relay_info.go | 17 - .../oai_chat/to_oai_responses_policy.go | 22 - service/relayconvert/request_compat.go | 57 --- service/request_converter.go | 14 +- service/sensitive.go | 2 +- service/task_billing.go | 2 +- service/task_billing_test.go | 2 +- service/task_polling.go | 9 +- service/task_polling_test.go | 15 +- service/text_quota.go | 4 +- service/text_quota_test.go | 4 +- service/tiered_settle.go | 2 +- service/tiered_settle_test.go | 2 +- service/token_counter.go | 4 +- service/usage_helpr.go | 2 +- service/user_notify.go | 2 +- service/violation_fee.go | 2 +- service/webhook.go | 2 +- setting/model_setting/claude.go | 22 + setting/model_setting/claude_test.go | 32 ++ setting/model_setting/gemini.go | 45 +- setting/model_setting/gemini_test.go | 99 ++++ .../operation_setting/status_code_ranges.go | 2 +- setting/payment_waffo.go | 24 +- setting/ratio_setting/cache_ratio.go | 2 +- setting/ratio_setting/group_ratio.go | 2 +- setting/ratio_setting/model_ratio.go | 2 +- setting/reasoning/suffix.go | 60 +-- 368 files changed, 7144 insertions(+), 1594 deletions(-) create mode 100644 controller/option_claude_test.go create mode 100644 controller/option_gemini_test.go create mode 100644 relay/common/relay_info_gemini_settings_test.go rename {dto => relaykit/dto}/alpha_search_request.go (85%) rename {dto => relaykit/dto}/audio.go (95%) rename {dto => relaykit/dto}/billing_usage.go (100%) rename {dto => relaykit/dto}/billing_usage_test.go (96%) rename {dto => relaykit/dto}/channel_settings.go (96%) rename {dto => relaykit/dto}/channel_settings_test.go (97%) rename {dto => relaykit/dto}/claude.go (96%) rename {dto => relaykit/dto}/embedding.go (94%) rename {dto => relaykit/dto}/error.go (85%) rename {dto => relaykit/dto}/gemini.go (94%) rename {dto => relaykit/dto}/gemini_generation_config_test.go (87%) rename {dto => relaykit/dto}/gemini_isstream_test.go (85%) rename {dto => relaykit/dto}/gemini_response_test.go (67%) rename {dto => relaykit/dto}/notify.go (100%) rename {dto => relaykit/dto}/openai_compaction.go (90%) rename {dto => relaykit/dto}/openai_image.go (91%) rename {dto => relaykit/dto}/openai_request.go (95%) rename {dto => relaykit/dto}/openai_request_zero_value_test.go (92%) rename {dto => relaykit/dto}/openai_response.go (98%) rename {dto => relaykit/dto}/openai_responses_compaction_request.go (91%) rename {dto => relaykit/dto}/openai_video.go (100%) rename {dto => relaykit/dto}/playground.go (100%) rename {dto => relaykit/dto}/pricing.go (74%) rename {dto => relaykit/dto}/ratio_sync.go (100%) rename {dto => relaykit/dto}/realtime.go (98%) rename {dto => relaykit/dto}/request_common.go (70%) rename {dto => relaykit/dto}/rerank.go (91%) rename {dto => relaykit/dto}/sensitive.go (100%) rename {dto => relaykit/dto}/user_settings.go (100%) rename {dto => relaykit/dto}/values.go (100%) create mode 100644 relaykit/go.mod create mode 100644 relaykit/go.sum rename {relay => relaykit}/reasonmap/reasonmap.go (84%) create mode 100644 relaykit/relayconvert/boundary_test.go create mode 100644 relaykit/relayconvert/claude_default_max_tokens_test.go create mode 100644 relaykit/relayconvert/convmeta/format.go create mode 100644 relaykit/relayconvert/convmeta/meta.go create mode 100644 relaykit/relayconvert/convmeta/meta_test.go create mode 100644 relaykit/relayconvert/convmeta/options.go create mode 100644 relaykit/relayconvert/golden_test.go rename {service => relaykit}/relayconvert/internal/claude_messages/to_oai_chat_req.go (83%) rename {service => relaykit}/relayconvert/internal/claude_messages/to_oai_chat_resp.go (96%) rename {service => relaykit}/relayconvert/internal/gemini_chat/to_oai_chat_req.go (81%) rename {service => relaykit}/relayconvert/internal/gemini_chat/to_oai_chat_resp.go (90%) rename {service => relaykit}/relayconvert/internal/jsonutil/stringify.go (59%) rename {service => relaykit}/relayconvert/internal/media/media.go (75%) rename {service => relaykit}/relayconvert/internal/oai_chat/to_claude_messages_req.go (81%) rename {service => relaykit}/relayconvert/internal/oai_chat/to_claude_messages_resp.go (74%) rename {service => relaykit}/relayconvert/internal/oai_chat/to_claude_messages_resp_test.go (96%) rename {service => relaykit}/relayconvert/internal/oai_chat/to_gemini_chat_req.go (86%) rename {service => relaykit}/relayconvert/internal/oai_chat/to_gemini_chat_resp.go (93%) rename {service => relaykit}/relayconvert/internal/oai_chat/to_gemini_chat_resp_test.go (96%) rename {service => relaykit}/relayconvert/internal/oai_chat/to_oai_responses_req.go (88%) rename {service => relaykit}/relayconvert/internal/oai_chat/to_oai_responses_req_test.go (97%) rename {service => relaykit}/relayconvert/internal/oai_chat/to_oai_responses_resp.go (96%) rename {service => relaykit}/relayconvert/internal/oai_chat/to_oai_responses_resp_test.go (99%) rename {service => relaykit}/relayconvert/internal/oai_chat/to_oai_responses_stream_resp.go (99%) rename {service => relaykit}/relayconvert/internal/oai_responses/req_helpers.go (79%) rename {service => relaykit}/relayconvert/internal/oai_responses/to_claude_messages_req.go (80%) rename {service => relaykit}/relayconvert/internal/oai_responses/to_gemini_chat_req.go (79%) rename {service => relaykit}/relayconvert/internal/oai_responses/to_gemini_chat_req_preprocess.go (65%) rename {service => relaykit}/relayconvert/internal/oai_responses/to_oai_chat_req.go (83%) rename {service => relaykit}/relayconvert/internal/oai_responses/to_oai_chat_req_test.go (98%) rename {service => relaykit}/relayconvert/internal/oai_responses/to_oai_chat_resp.go (98%) rename {service => relaykit}/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go (99%) rename {service => relaykit}/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go (98%) rename {service => relaykit}/relayconvert/internal/shared/claude/cache.go (100%) create mode 100644 relaykit/relayconvert/internal/shared/claude/errors.go rename {service => relaykit}/relayconvert/internal/shared/claude/tool_choice.go (95%) rename {service => relaykit}/relayconvert/internal/shared/gemini/request.go (81%) rename {service => relaykit}/relayconvert/internal/shared/gemini/schema.go (99%) create mode 100644 relaykit/relayconvert/kitutil/json.go create mode 100644 relaykit/relayconvert/kitutil/log.go create mode 100644 relaykit/relayconvert/kitutil/log_test.go create mode 100644 relaykit/relayconvert/kitutil/mask.go create mode 100644 relaykit/relayconvert/kitutil/value.go rename {service => relaykit}/relayconvert/media.go (64%) create mode 100644 relaykit/relayconvert/reasoning/suffix.go create mode 100644 relaykit/relayconvert/request_compat.go rename {service => relaykit}/relayconvert/request_registry.go (84%) rename {service => relaykit}/relayconvert/request_registry_test.go (80%) rename {service => relaykit}/relayconvert/response_compat.go (88%) rename {service => relaykit}/relayconvert/response_registry.go (84%) rename {service => relaykit}/relayconvert/response_registry_test.go (95%) create mode 100644 relaykit/relayconvert/safety_settings_test.go create mode 100644 relaykit/relayconvert/testdata/golden/request/claude_to_gemini.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/request/claude_to_openai.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/request/claude_to_openai_responses.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/request/gemini_to_claude.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/request/gemini_to_openai.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/request/gemini_to_openai_responses.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/request/openai_responses_to_claude.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/request/openai_responses_to_gemini.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/request/openai_responses_to_openai.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/request/openai_to_claude.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/request/openai_to_gemini.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/request/openai_to_openai_responses.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/response/claude_to_gemini.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/response/claude_to_openai.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/response/claude_to_openai_responses.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/response/gemini_to_claude.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/response/gemini_to_openai.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/response/gemini_to_openai_responses.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/response/openai_responses_to_claude.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/response/openai_responses_to_gemini.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/response/openai_responses_to_openai.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/response/openai_to_claude.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/response/openai_to_gemini.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/response/openai_to_openai_responses.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/stream/claude_to_gemini.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/stream/claude_to_openai.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/stream/claude_to_openai_responses.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/stream/gemini_to_claude.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/stream/gemini_to_openai.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/stream/gemini_to_openai_responses.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/stream/openai_responses_to_claude.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/stream/openai_responses_to_gemini.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/stream/openai_responses_to_openai.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/stream/openai_to_claude.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/stream/openai_to_gemini.golden.json create mode 100644 relaykit/relayconvert/testdata/golden/stream/openai_to_openai_responses.golden.json rename {service => relaykit}/relayconvert/text_converter_registry.go (99%) rename {service => relaykit}/relayconvert/text_converter_registry_test.go (99%) rename {types => relaykit/types}/channel_error.go (100%) create mode 100644 relaykit/types/endpoint_type.go rename {types => relaykit/types}/error.go (97%) rename {types => relaykit/types}/file_data.go (100%) rename {types => relaykit/types}/file_source.go (100%) rename {types => relaykit/types}/price_data.go (100%) rename {types => relaykit/types}/relay_format.go (100%) rename {types => relaykit/types}/request_meta.go (100%) rename {types => relaykit/types}/rw_map.go (88%) rename {types => relaykit/types}/set.go (100%) delete mode 100644 service/relayconvert/internal/matcher/regex.go delete mode 100644 service/relayconvert/internal/meta/relay_info.go delete mode 100644 service/relayconvert/internal/oai_chat/to_oai_responses_policy.go delete mode 100644 service/relayconvert/request_compat.go create mode 100644 setting/model_setting/gemini_test.go diff --git a/.gitignore b/.gitignore index 25f8469e..dc328dd6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ skills-lock.json .local-tests/ service/relayconvert/chat_responses_live_local_test.go service/openaicompat/chat_responses_live_local_test.go +go.work +go.work.sum diff --git a/Dockerfile b/Dockerfile index 10345a5c..5be311b1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ COPY ./VERSION /build/VERSION RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat /build/VERSION) bun run build FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder2 -ENV GO111MODULE=on CGO_ENABLED=0 +ENV GO111MODULE=on CGO_ENABLED=0 GOWORK=off ARG TARGETOS ARG TARGETARCH @@ -18,6 +18,9 @@ ENV GOEXPERIMENT=greenteagc WORKDIR /build ADD go.mod go.sum ./ +# relaykit is a local submodule referenced via replace; its go.mod must be +# present for go mod download to resolve the main module graph. +ADD relaykit/go.mod ./relaykit/go.mod RUN go mod download COPY . . diff --git a/common/str.go b/common/str.go index 9f3b9d46..b412c7dc 100644 --- a/common/str.go +++ b/common/str.go @@ -4,8 +4,7 @@ import ( "encoding/base64" "encoding/json" "fmt" - "net/url" - "regexp" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" "strconv" "strings" "unsafe" @@ -13,14 +12,6 @@ import ( "github.com/samber/lo" ) -var ( - maskURLPattern = regexp.MustCompile(`(http|https)://[^\s/$.?#].[^\s]*`) - maskDomainPattern = regexp.MustCompile(`\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}\b`) - maskIPPattern = regexp.MustCompile(`\b(?:\d{1,3}\.){3}\d{1,3}\b`) - // maskApiKeyPattern matches patterns like 'api_key:xxx' or "api_key:xxx" to mask the API key value - maskApiKeyPattern = regexp.MustCompile(`(['"]?)api_key:([^\s'"]+)(['"]?)`) -) - const LocalLogContentLimit = 2048 // LocalLogPreview limits log-only content unless debug logging is enabled. @@ -145,121 +136,8 @@ func MaskEmail(email string) string { return "***@" + email[atIndex+1:] } -// maskHostTail returns the tail parts of a domain/host that should be preserved. -// It keeps 2 parts for likely country-code TLDs (e.g., co.uk, com.cn), otherwise keeps only the TLD. -func maskHostTail(parts []string) []string { - if len(parts) < 2 { - return parts - } - lastPart := parts[len(parts)-1] - secondLastPart := parts[len(parts)-2] - if len(lastPart) == 2 && len(secondLastPart) <= 3 { - // Likely country code TLD like co.uk, com.cn - return []string{secondLastPart, lastPart} - } - return []string{lastPart} -} - -// maskHostForURL collapses subdomains and keeps only masked prefix + preserved tail. -// Example: api.openai.com -> ***.com, sub.domain.co.uk -> ***.co.uk -func maskHostForURL(host string) string { - parts := strings.Split(host, ".") - if len(parts) < 2 { - return "***" - } - tail := maskHostTail(parts) - return "***." + strings.Join(tail, ".") -} - -// maskHostForPlainDomain masks a plain domain and reflects subdomain depth with multiple ***. -// Example: openai.com -> ***.com, api.openai.com -> ***.***.com, sub.domain.co.uk -> ***.***.co.uk -func maskHostForPlainDomain(domain string) string { - parts := strings.Split(domain, ".") - if len(parts) < 2 { - return domain - } - tail := maskHostTail(parts) - numStars := len(parts) - len(tail) - if numStars < 1 { - numStars = 1 - } - stars := strings.TrimSuffix(strings.Repeat("***.", numStars), ".") - return stars + "." + strings.Join(tail, ".") -} - -// MaskSensitiveInfo masks sensitive information like URLs, IPs, and domain names in a string -// Example: -// http://example.com -> http://***.com -// https://api.test.org/v1/users/123?key=secret -> https://***.org/***/***/?key=*** -// https://sub.domain.co.uk/path/to/resource -> https://***.co.uk/***/*** -// 192.168.1.1 -> ***.***.***.*** -// openai.com -> ***.com -// www.openai.com -> ***.***.com -// api.openai.com -> ***.***.com +// MaskSensitiveInfo moved to the conversion kit (kitutil) because the types +// package error formatting depends on it; host callers keep this name. func MaskSensitiveInfo(str string) string { - // Mask URLs - str = maskURLPattern.ReplaceAllStringFunc(str, func(urlStr string) string { - u, err := url.Parse(urlStr) - if err != nil { - return urlStr - } - - host := u.Host - if host == "" { - return urlStr - } - - // Mask host with unified logic - maskedHost := maskHostForURL(host) - - result := u.Scheme + "://" + maskedHost - - // Mask path - if u.Path != "" && u.Path != "/" { - pathParts := strings.Split(strings.Trim(u.Path, "/"), "/") - maskedPathParts := make([]string, len(pathParts)) - for i := range pathParts { - if pathParts[i] != "" { - maskedPathParts[i] = "***" - } - } - if len(maskedPathParts) > 0 { - result += "/" + strings.Join(maskedPathParts, "/") - } - } else if u.Path == "/" { - result += "/" - } - - // Mask query parameters - if u.RawQuery != "" { - values, err := url.ParseQuery(u.RawQuery) - if err != nil { - // If can't parse query, just mask the whole query string - result += "?***" - } else { - maskedParams := make([]string, 0, len(values)) - for key := range values { - maskedParams = append(maskedParams, key+"=***") - } - if len(maskedParams) > 0 { - result += "?" + strings.Join(maskedParams, "&") - } - } - } - - return result - }) - - // Mask domain names without protocol (like openai.com, www.openai.com) - str = maskDomainPattern.ReplaceAllStringFunc(str, func(domain string) string { - return maskHostForPlainDomain(domain) - }) - - // Mask IP addresses - str = maskIPPattern.ReplaceAllString(str, "***.***.***.***") - - // Mask API keys (e.g., "api_key:AIzaSyAAAaUooTUni8AdaOkSRMda30n_Q4vrV70" -> "api_key:***") - str = maskApiKeyPattern.ReplaceAllString(str, "${1}api_key:***${3}") - - return str + return kitutil.MaskSensitiveInfo(str) } diff --git a/constant/endpoint_type.go b/constant/endpoint_type.go index 5d27956d..50eed0d1 100644 --- a/constant/endpoint_type.go +++ b/constant/endpoint_type.go @@ -1,20 +1,20 @@ package constant -type EndpointType string +import "github.com/QuantumNous/new-api/relaykit/types" + +// EndpointType moved to types with the conversion kit; aliases keep host +// code compiling unchanged. +type EndpointType = types.EndpointType const ( - EndpointTypeOpenAI EndpointType = "openai" - EndpointTypeOpenAIResponse EndpointType = "openai-response" - EndpointTypeOpenAIResponseCompact EndpointType = "openai-response-compact" - EndpointTypeOpenAIAlphaSearch EndpointType = "openai-alpha-search" - EndpointTypeAnthropic EndpointType = "anthropic" - EndpointTypeGemini EndpointType = "gemini" - EndpointTypeJinaRerank EndpointType = "jina-rerank" - EndpointTypeImageGeneration EndpointType = "image-generation" - EndpointTypeEmbeddings EndpointType = "embeddings" - EndpointTypeOpenAIVideo EndpointType = "openai-video" - //EndpointTypeMidjourney EndpointType = "midjourney-proxy" - //EndpointTypeSuno EndpointType = "suno-proxy" - //EndpointTypeKling EndpointType = "kling" - //EndpointTypeJimeng EndpointType = "jimeng" + EndpointTypeOpenAI = types.EndpointTypeOpenAI + EndpointTypeOpenAIResponse = types.EndpointTypeOpenAIResponse + EndpointTypeOpenAIResponseCompact = types.EndpointTypeOpenAIResponseCompact + EndpointTypeOpenAIAlphaSearch = types.EndpointTypeOpenAIAlphaSearch + EndpointTypeAnthropic = types.EndpointTypeAnthropic + EndpointTypeGemini = types.EndpointTypeGemini + EndpointTypeJinaRerank = types.EndpointTypeJinaRerank + EndpointTypeImageGeneration = types.EndpointTypeImageGeneration + EndpointTypeEmbeddings = types.EndpointTypeEmbeddings + EndpointTypeOpenAIVideo = types.EndpointTypeOpenAIVideo ) diff --git a/constant/finish_reason.go b/constant/finish_reason.go index 5a752a5f..9d751be1 100644 --- a/constant/finish_reason.go +++ b/constant/finish_reason.go @@ -1,9 +1,12 @@ package constant +import "github.com/QuantumNous/new-api/relaykit/types" + +// Finish reasons moved to types with the conversion kit. var ( - FinishReasonStop = "stop" - FinishReasonToolCalls = "tool_calls" - FinishReasonLength = "length" - FinishReasonFunctionCall = "function_call" - FinishReasonContentFilter = "content_filter" + FinishReasonStop = types.FinishReasonStop + FinishReasonToolCalls = types.FinishReasonToolCalls + FinishReasonLength = types.FinishReasonLength + FinishReasonFunctionCall = types.FinishReasonFunctionCall + FinishReasonContentFilter = types.FinishReasonContentFilter ) diff --git a/constant/waffo_pay_method.go b/constant/waffo_pay_method.go index 0cee72a8..b196554e 100644 --- a/constant/waffo_pay_method.go +++ b/constant/waffo_pay_method.go @@ -2,8 +2,8 @@ package constant // WaffoPayMethod defines the display and API parameter mapping for Waffo payment methods. type WaffoPayMethod struct { - Name string `json:"name"` // Frontend display name - Icon string `json:"icon"` // Frontend icon identifier: credit-card, apple, google + Name string `json:"name"` // Frontend display name + Icon string `json:"icon"` // Frontend icon identifier: credit-card, apple, google PayMethodType string `json:"payMethodType"` // Waffo API PayMethodType, can be comma-separated PayMethodName string `json:"payMethodName"` // Waffo API PayMethodName, empty means auto-select by Waffo checkout } diff --git a/controller/billing.go b/controller/billing.go index f75f6819..aed5bf4f 100644 --- a/controller/billing.go +++ b/controller/billing.go @@ -3,8 +3,8 @@ package controller import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 2d2b4f6e..62982d2f 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -12,9 +12,9 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/types" "github.com/shopspring/decimal" diff --git a/controller/channel-test.go b/controller/channel-test.go index 153ccb0d..c705db0f 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -16,7 +16,6 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/middleware" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/pkg/billingexpr" @@ -24,10 +23,11 @@ import ( relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/QuantumNous/new-api/setting/ratio_setting" - "github.com/QuantumNous/new-api/types" "github.com/samber/lo" "github.com/tidwall/gjson" diff --git a/controller/channel.go b/controller/channel.go index 8b7170d9..3a1e5832 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -11,12 +11,12 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/model" relaychannel "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/ollama" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/service/authz" diff --git a/controller/channel_test_internal_test.go b/controller/channel_test_internal_test.go index 89d0ed59..3af30186 100644 --- a/controller/channel_test_internal_test.go +++ b/controller/channel_test_internal_test.go @@ -9,13 +9,13 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/pkg/billingexpr" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/controller/channel_upstream_update.go b/controller/channel_upstream_update.go index 19db4345..71ab0e53 100644 --- a/controller/channel_upstream_update.go +++ b/controller/channel_upstream_update.go @@ -15,15 +15,15 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/relay/channel/advancedcustom" "github.com/QuantumNous/new-api/relay/channel/gemini" "github.com/QuantumNous/new-api/relay/channel/ollama" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/samber/lo" diff --git a/controller/channel_upstream_update_test.go b/controller/channel_upstream_update_test.go index fd373fcc..0a524d70 100644 --- a/controller/channel_upstream_update_test.go +++ b/controller/channel_upstream_update_test.go @@ -10,8 +10,8 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/controller/model.go b/controller/model.go index 09ea70fc..b32eebd7 100644 --- a/controller/model.go +++ b/controller/model.go @@ -8,7 +8,6 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/relay" "github.com/QuantumNous/new-api/relay/channel/ai360" @@ -17,9 +16,10 @@ import ( "github.com/QuantumNous/new-api/relay/channel/moonshot" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/samber/lo" ) diff --git a/controller/model_list_test.go b/controller/model_list_test.go index 9c80d177..b1fa9b95 100644 --- a/controller/model_list_test.go +++ b/controller/model_list_test.go @@ -10,8 +10,8 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting/config" "github.com/QuantumNous/new-api/setting/operation_setting" diff --git a/controller/option.go b/controller/option.go index 0367c793..940bb306 100644 --- a/controller/option.go +++ b/controller/option.go @@ -11,6 +11,7 @@ import ( "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting/console_setting" + "github.com/QuantumNous/new-api/setting/model_setting" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/QuantumNous/new-api/setting/ratio_setting" "github.com/QuantumNous/new-api/setting/system_setting" @@ -235,6 +236,24 @@ func UpdateOption(c *gin.Context) { }) return } + case "gemini.safety_settings": + err = model_setting.ValidateGeminiSafetySettings(option.Value.(string)) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + case "claude.default_max_tokens": + err = model_setting.ValidateClaudeDefaultMaxTokens(option.Value.(string)) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } case operation_setting.ToolPriceOptionKey: err = operation_setting.ValidateToolPricesJSON(option.Value.(string)) if err != nil { diff --git a/controller/option_claude_test.go b/controller/option_claude_test.go new file mode 100644 index 00000000..db930993 --- /dev/null +++ b/controller/option_claude_test.go @@ -0,0 +1,34 @@ +package controller + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpdateOptionRejectsNegativeClaudeDefaultMaxTokens(t *testing.T) { + response := httptest.NewRecorder() + context, _ := gin.CreateTestContext(response) + context.Request = httptest.NewRequest( + http.MethodPut, + "/api/option/", + strings.NewReader(`{"key":"claude.default_max_tokens","value":"{\"default\":-1}"}`), + ) + + UpdateOption(context) + + assert.Equal(t, http.StatusOK, response.Code) + var payload struct { + Success bool `json:"success"` + Message string `json:"message"` + } + require.NoError(t, common.Unmarshal(response.Body.Bytes(), &payload)) + assert.False(t, payload.Success) + assert.Contains(t, payload.Message, "-1") +} diff --git a/controller/option_gemini_test.go b/controller/option_gemini_test.go new file mode 100644 index 00000000..60632be9 --- /dev/null +++ b/controller/option_gemini_test.go @@ -0,0 +1,34 @@ +package controller + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpdateOptionRejectsInvalidGeminiSafetyThreshold(t *testing.T) { + response := httptest.NewRecorder() + context, _ := gin.CreateTestContext(response) + context.Request = httptest.NewRequest( + http.MethodPut, + "/api/option/", + strings.NewReader(`{"key":"gemini.safety_settings","value":"{\"default\":\"BLOCK_SOME\"}"}`), + ) + + UpdateOption(context) + + assert.Equal(t, http.StatusOK, response.Code) + var payload struct { + Success bool `json:"success"` + Message string `json:"message"` + } + require.NoError(t, common.Unmarshal(response.Body.Bytes(), &payload)) + assert.False(t, payload.Success) + assert.Contains(t, payload.Message, "BLOCK_SOME") +} diff --git a/controller/playground.go b/controller/playground.go index 501c4e15..1c9c8d3b 100644 --- a/controller/playground.go +++ b/controller/playground.go @@ -7,7 +7,7 @@ import ( "github.com/QuantumNous/new-api/middleware" "github.com/QuantumNous/new-api/model" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/controller/ratio_sync.go b/controller/ratio_sync.go index 1f57bcc2..0001a60b 100644 --- a/controller/ratio_sync.go +++ b/controller/ratio_sync.go @@ -19,8 +19,8 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/logger" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting/billing_setting" "github.com/QuantumNous/new-api/setting/ratio_setting" "github.com/samber/lo" diff --git a/controller/relay.go b/controller/relay.go index a214ea3f..5a321319 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -11,7 +11,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + taskdto "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/middleware" "github.com/QuantumNous/new-api/model" @@ -20,10 +20,11 @@ import ( relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/types" "github.com/bytedance/gopkg/util/gopool" "github.com/samber/lo" @@ -415,7 +416,7 @@ func RelayMidjourney(c *gin.Context) { return } - var mjErr *dto.MidjourneyResponse + var mjErr *taskdto.MidjourneyResponse switch relayInfo.RelayMode { case relayconstant.RelayModeMidjourneyNotify: mjErr = relay.RelayMidjourneyNotify(c) @@ -473,7 +474,7 @@ func RelayNotFound(c *gin.Context) { func RelayTaskFetch(c *gin.Context) { relayInfo, err := relaycommon.GenRelayInfo(c, types.RelayFormatTask, nil, nil) if err != nil { - c.JSON(http.StatusInternalServerError, &dto.TaskError{ + c.JSON(http.StatusInternalServerError, &taskdto.TaskError{ Code: "gen_relay_info_failed", Message: err.Error(), StatusCode: http.StatusInternalServerError, @@ -488,7 +489,7 @@ func RelayTaskFetch(c *gin.Context) { func RelayTask(c *gin.Context) { relayInfo, err := relaycommon.GenRelayInfo(c, types.RelayFormatTask, nil, nil) if err != nil { - c.JSON(http.StatusInternalServerError, &dto.TaskError{ + c.JSON(http.StatusInternalServerError, &taskdto.TaskError{ Code: "gen_relay_info_failed", Message: err.Error(), StatusCode: http.StatusInternalServerError, @@ -502,7 +503,7 @@ func RelayTask(c *gin.Context) { } var result *relay.TaskSubmitResult - var taskErr *dto.TaskError + var taskErr *taskdto.TaskError defer func() { if taskErr != nil && relayInfo.Billing != nil { relayInfo.Billing.Refund(c) @@ -608,14 +609,14 @@ func RelayTask(c *gin.Context) { } // respondTaskError 统一输出 Task 错误响应(含 429 限流提示改写) -func respondTaskError(c *gin.Context, taskErr *dto.TaskError) { +func respondTaskError(c *gin.Context, taskErr *taskdto.TaskError) { if taskErr.StatusCode == http.StatusTooManyRequests { taskErr.Message = "当前分组上游负载已饱和,请稍后再试" } c.JSON(taskErr.StatusCode, taskErr) } -func shouldRetryTaskRelay(c *gin.Context, channelId int, taskErr *dto.TaskError, retryTimes int) bool { +func shouldRetryTaskRelay(c *gin.Context, channelId int, taskErr *taskdto.TaskError, retryTimes int) bool { if taskErr == nil { return false } diff --git a/controller/task.go b/controller/task.go index a80f1a68..cb466a85 100644 --- a/controller/task.go +++ b/controller/task.go @@ -8,7 +8,7 @@ import ( "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/relay" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/controller/user.go b/controller/user.go index b8be50f7..59aaf584 100644 --- a/controller/user.go +++ b/controller/user.go @@ -11,11 +11,11 @@ import ( "time" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/middleware" "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/service/authz" "github.com/QuantumNous/new-api/setting" diff --git a/go.mod b/go.mod index 98f291f6..470e892b 100644 --- a/go.mod +++ b/go.mod @@ -36,14 +36,14 @@ require ( github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.5.0 github.com/samber/hot v0.11.0 - github.com/samber/lo v1.52.0 + github.com/samber/lo v1.53.0 github.com/shirou/gopsutil v3.21.11+incompatible github.com/shopspring/decimal v1.4.0 github.com/stretchr/testify v1.11.1 github.com/stripe/stripe-go/v81 v81.4.0 github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 github.com/thanhpk/randstr v1.0.6 - github.com/tidwall/gjson v1.18.0 + github.com/tidwall/gjson v1.19.0 github.com/tidwall/sjson v1.2.5 github.com/tiktoken-go/tokenizer v0.6.2 github.com/waffo-com/waffo-go v1.3.2 @@ -162,3 +162,7 @@ require ( modernc.org/memory v1.11.0 // indirect modernc.org/sqlite v1.40.1 // indirect ) + +require github.com/QuantumNous/new-api/relaykit v0.0.0 + +replace github.com/QuantumNous/new-api/relaykit => ./relaykit diff --git a/go.sum b/go.sum index ca6bd091..357374c3 100644 --- a/go.sum +++ b/go.sum @@ -1841,8 +1841,8 @@ github.com/samber/go-singleflightx v0.3.2 h1:jXbUU0fvis8Fdv4HGONboX5WdEZcYLoBEcK github.com/samber/go-singleflightx v0.3.2/go.mod h1:X2BR+oheHIYc73PvxRMlcASg6KYYTQyUYpdVU7t/ux4= github.com/samber/hot v0.11.0 h1:JhV9hk8SmZIqB0To8OyCzPubvszkuoSXWx/7FCEGO+Q= github.com/samber/hot v0.11.0/go.mod h1:NB9v5U4NfDx7jmlrP+zHuqCuLUsywgAtCH7XOAkOxAg= -github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= -github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= @@ -1946,8 +1946,8 @@ github.com/testcontainers/testcontainers-go v0.25.0/go.mod h1:4sC9SiJyzD1XFi59q8 github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o= github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= +github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= diff --git a/i18n/i18n.go b/i18n/i18n.go index 7ca8d2aa..b4edf2eb 100644 --- a/i18n/i18n.go +++ b/i18n/i18n.go @@ -12,7 +12,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" ) const ( diff --git a/main.go b/main.go index 548034a1..69d83c32 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ import ( "github.com/QuantumNous/new-api/oauth" perfmetrics "github.com/QuantumNous/new-api/pkg/perf_metrics" "github.com/QuantumNous/new-api/relay" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" "github.com/QuantumNous/new-api/router" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/service/authz" @@ -46,6 +47,10 @@ var indexPage []byte func main() { startTime := time.Now() + kitutil.SetLogging(common.SysLog, func(message string) { + logger.LogError(nil, message) + }) + kitutil.SetSystemErrorLogging(common.SysError) err := InitResources() if err != nil { @@ -61,6 +66,8 @@ func main() { common.SysLog("running in debug mode") } + kitutil.Debug.Store(common.DebugEnabled) + defer func() { err := model.CloseDB() if err != nil { diff --git a/makefile b/makefile index bcbd02b8..24048b91 100644 --- a/makefile +++ b/makefile @@ -8,7 +8,7 @@ DEV_POSTGRES_DB = new-api DEV_POSTGRES_USER = root DEV_SQLITE_PATH ?= one-api.db -.PHONY: all build-web build-all-web start-api dev dev-api dev-api-rebuild dev-web reset-setup +.PHONY: all build-web build-all-web start-api dev dev-api dev-api-rebuild dev-web reset-setup test all: build-all-web start-api @@ -39,6 +39,15 @@ dev-web: dev: dev-api dev-web +# The main package embeds the ignored web/dist output and is covered after build-web. +test: + @echo "Testing root Go module..." + @root_module=$$(GOWORK=off go list -m); \ + root_packages=$$(GOWORK=off go list -e ./... | grep -vxF "$$root_module"); \ + GOWORK=off go test $$root_packages + @echo "Testing relaykit Go module..." + @cd relaykit && GOWORK=off go test ./... + reset-setup: @echo "Resetting local setup wizard state..." @if docker compose -f $(DEV_COMPOSE_FILE) ps --services --status running | grep -qx "$(DEV_POSTGRES_SERVICE)"; then \ diff --git a/middleware/auth.go b/middleware/auth.go index 170f5fb3..2ad09a7a 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -12,10 +12,10 @@ import ( "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/service/authz" "github.com/QuantumNous/new-api/setting/ratio_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "gorm.io/gorm" diff --git a/middleware/distributor.go b/middleware/distributor.go index 4234011c..bde639dd 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -12,13 +12,14 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + taskdto "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/model" relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/ratio_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/tidwall/gjson" @@ -261,7 +262,7 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) { relayMode == relayconstant.RelayModeMidjourneyTaskImageSeed { shouldSelectChannel = false } else { - midjourneyRequest := dto.MidjourneyRequest{} + midjourneyRequest := taskdto.MidjourneyRequest{} err = common.UnmarshalBodyReusable(c, &midjourneyRequest) if err != nil { return nil, false, errors.New(i18n.T(c, i18n.MsgDistributorInvalidMidjourney, map[string]any{"Error": err.Error()})) diff --git a/middleware/i18n.go b/middleware/i18n.go index 279a738a..df8282ce 100644 --- a/middleware/i18n.go +++ b/middleware/i18n.go @@ -5,8 +5,8 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/i18n" + "github.com/QuantumNous/new-api/relaykit/dto" ) // I18n middleware detects and sets the language preference for the request diff --git a/middleware/performance.go b/middleware/performance.go index 8668bb95..9cef7f4d 100644 --- a/middleware/performance.go +++ b/middleware/performance.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/middleware/utils.go b/middleware/utils.go index f198af81..a4c981d1 100644 --- a/middleware/utils.go +++ b/middleware/utils.go @@ -5,7 +5,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/logger" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/model/ability.go b/model/ability.go index e67b2830..d950a6ad 100644 --- a/model/ability.go +++ b/model/ability.go @@ -8,7 +8,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/samber/lo" "gorm.io/gorm" diff --git a/model/channel.go b/model/channel.go index 1de87687..f7cf5094 100644 --- a/model/channel.go +++ b/model/channel.go @@ -11,9 +11,9 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/samber/lo" "gorm.io/gorm" diff --git a/model/channel_cache.go b/model/channel_cache.go index 81923017..86c59438 100644 --- a/model/channel_cache.go +++ b/model/channel_cache.go @@ -11,8 +11,8 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting/ratio_setting" ) diff --git a/model/channel_settings_test.go b/model/channel_settings_test.go index c4974faf..7203671f 100644 --- a/model/channel_settings_test.go +++ b/model/channel_settings_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/model/log.go b/model/log.go index 401d53c4..a6773fcb 100644 --- a/model/log.go +++ b/model/log.go @@ -9,7 +9,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/logger" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" diff --git a/model/pricing.go b/model/pricing.go index 440e1e09..0bfb1a3b 100644 --- a/model/pricing.go +++ b/model/pricing.go @@ -9,10 +9,10 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/billing_setting" "github.com/QuantumNous/new-api/setting/ratio_setting" - "github.com/QuantumNous/new-api/types" ) type Pricing struct { diff --git a/model/pricing_endpoint_test.go b/model/pricing_endpoint_test.go index eeca35e6..fa67da0e 100644 --- a/model/pricing_endpoint_test.go +++ b/model/pricing_endpoint_test.go @@ -6,7 +6,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/model/task.go b/model/task.go index b50d45b9..9a178358 100644 --- a/model/task.go +++ b/model/task.go @@ -8,8 +8,8 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" commonRelay "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" ) type TaskStatus string diff --git a/model/user.go b/model/user.go index 75531a26..b25de5e7 100644 --- a/model/user.go +++ b/model/user.go @@ -8,8 +8,8 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/bytedance/gopkg/util/gopool" diff --git a/model/user_cache.go b/model/user_cache.go index 2ca84f40..aa72fc26 100644 --- a/model/user_cache.go +++ b/model/user_cache.go @@ -6,7 +6,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/gin-gonic/gin" ) diff --git a/model/user_update_test.go b/model/user_update_test.go index be232693..c68d8f33 100644 --- a/model/user_update_test.go +++ b/model/user_update_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/billingexpr/compile.go b/pkg/billingexpr/compile.go index c41aed75..a6c7b8f7 100644 --- a/pkg/billingexpr/compile.go +++ b/pkg/billingexpr/compile.go @@ -39,30 +39,30 @@ var ( // compileEnvPrototypeV1 is the v1 type-checking prototype used at compile time. var compileEnvPrototypeV1 = map[string]interface{}{ - "p": float64(0), - "c": float64(0), - "len": float64(0), - "cr": float64(0), - "cc": float64(0), - "cc1h": float64(0), - "img": float64(0), - "img_o": float64(0), - "ai": float64(0), - "ao": float64(0), - "tier": func(string, float64) float64 { return 0 }, - "header": func(string) string { return "" }, - "param": func(string) interface{} { return nil }, - "has": func(interface{}, string) bool { return false }, - "hour": func(string) int { return 0 }, - "minute": func(string) int { return 0 }, - "weekday": func(string) int { return 0 }, - "month": func(string) int { return 0 }, - "day": func(string) int { return 0 }, - "max": math.Max, - "min": math.Min, - "abs": math.Abs, - "ceil": math.Ceil, - "floor": math.Floor, + "p": float64(0), + "c": float64(0), + "len": float64(0), + "cr": float64(0), + "cc": float64(0), + "cc1h": float64(0), + "img": float64(0), + "img_o": float64(0), + "ai": float64(0), + "ao": float64(0), + "tier": func(string, float64) float64 { return 0 }, + "header": func(string) string { return "" }, + "param": func(string) interface{} { return nil }, + "has": func(interface{}, string) bool { return false }, + "hour": func(string) int { return 0 }, + "minute": func(string) int { return 0 }, + "weekday": func(string) int { return 0 }, + "month": func(string) int { return 0 }, + "day": func(string) int { return 0 }, + "max": math.Max, + "min": math.Min, + "abs": math.Abs, + "ceil": math.Ceil, + "floor": math.Floor, } func getCompileEnv(version int) map[string]interface{} { diff --git a/pkg/billingexpr/run.go b/pkg/billingexpr/run.go index d477d44e..7c0f2ecd 100644 --- a/pkg/billingexpr/run.go +++ b/pkg/billingexpr/run.go @@ -53,16 +53,16 @@ func runProgram(prog *vm.Program, params TokenParams, request RequestInput) (flo headers := normalizeHeaders(request.Headers) env := map[string]interface{}{ - "p": params.P, - "c": params.C, - "len": params.Len, - "cr": params.CR, - "cc": params.CC, - "cc1h": params.CC1h, - "img": params.Img, + "p": params.P, + "c": params.C, + "len": params.Len, + "cr": params.CR, + "cc": params.CC, + "cc1h": params.CC1h, + "img": params.Img, "img_o": params.ImgO, - "ai": params.AI, - "ao": params.AO, + "ai": params.AI, + "ao": params.AO, "tier": func(name string, value float64) float64 { trace.MatchedTier = name trace.Cost = value @@ -94,10 +94,10 @@ func runProgram(prog *vm.Program, params TokenParams, request RequestInput) (flo "month": func(tz string) int { return int(timeInZone(tz).Month()) }, "day": func(tz string) int { return timeInZone(tz).Day() }, "max": math.Max, - "min": math.Min, - "abs": math.Abs, - "ceil": math.Ceil, - "floor": math.Floor, + "min": math.Min, + "abs": math.Abs, + "ceil": math.Ceil, + "floor": math.Floor, } out, err := expr.Run(prog, env) diff --git a/relay/alpha_search_handler.go b/relay/alpha_search_handler.go index da0675da..23ebafea 100644 --- a/relay/alpha_search_handler.go +++ b/relay/alpha_search_handler.go @@ -8,12 +8,12 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/audio_handler.go b/relay/audio_handler.go index 7e9f6c48..bb9ff840 100644 --- a/relay/audio_handler.go +++ b/relay/audio_handler.go @@ -6,11 +6,11 @@ import ( "net/http" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/adapter.go b/relay/channel/adapter.go index d2f7c6bb..3735b6ad 100644 --- a/relay/channel/adapter.go +++ b/relay/channel/adapter.go @@ -4,10 +4,11 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" + taskdto "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) @@ -34,7 +35,7 @@ type Adaptor interface { type TaskAdaptor interface { Init(info *relaycommon.RelayInfo) - ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) *dto.TaskError + ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) *taskdto.TaskError // ── Billing ────────────────────────────────────────────────────── @@ -67,7 +68,7 @@ type TaskAdaptor interface { BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) - DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, err *dto.TaskError) + DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, err *taskdto.TaskError) GetModelList() []string GetChannelName() string diff --git a/relay/channel/advancedcustom/adaptor.go b/relay/channel/advancedcustom/adaptor.go index f6bf6145..74af2d5c 100644 --- a/relay/channel/advancedcustom/adaptor.go +++ b/relay/channel/advancedcustom/adaptor.go @@ -9,16 +9,16 @@ import ( "strings" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/claude" "github.com/QuantumNous/new-api/relay/channel/gemini" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/service/relayconvert" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/samber/lo" ) diff --git a/relay/channel/advancedcustom/adaptor_test.go b/relay/channel/advancedcustom/adaptor_test.go index 6f59ed22..5a11a972 100644 --- a/relay/channel/advancedcustom/adaptor_test.go +++ b/relay/channel/advancedcustom/adaptor_test.go @@ -10,12 +10,12 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/service/relayconvert" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/relay/channel/ali/adaptor.go b/relay/channel/ali/adaptor.go index d2f7d219..ba377659 100644 --- a/relay/channel/ali/adaptor.go +++ b/relay/channel/ali/adaptor.go @@ -8,15 +8,15 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/claude" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/samber/lo" diff --git a/relay/channel/ali/dto.go b/relay/channel/ali/dto.go index ec564f08..58df8761 100644 --- a/relay/channel/ali/dto.go +++ b/relay/channel/ali/dto.go @@ -3,8 +3,8 @@ package ali import ( "strings" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/service" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/ali/image.go b/relay/channel/ali/image.go index af0717a3..6913fa34 100644 --- a/relay/channel/ali/image.go +++ b/relay/channel/ali/image.go @@ -11,11 +11,11 @@ import ( "time" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/samber/lo" diff --git a/relay/channel/ali/image_wan.go b/relay/channel/ali/image_wan.go index e2f46060..beaf9819 100644 --- a/relay/channel/ali/image_wan.go +++ b/relay/channel/ali/image_wan.go @@ -5,8 +5,8 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/gin-gonic/gin" "github.com/samber/lo" diff --git a/relay/channel/ali/rerank.go b/relay/channel/ali/rerank.go index 1f7a3451..ac2afbd3 100644 --- a/relay/channel/ali/rerank.go +++ b/relay/channel/ali/rerank.go @@ -5,10 +5,10 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/ali/text.go b/relay/channel/ali/text.go index 09a52adb..6e532f9d 100644 --- a/relay/channel/ali/text.go +++ b/relay/channel/ali/text.go @@ -1,7 +1,7 @@ package ali import ( - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/samber/lo" ) diff --git a/relay/channel/api_request.go b/relay/channel/api_request.go index 62de6a32..f6c79925 100644 --- a/relay/channel/api_request.go +++ b/relay/channel/api_request.go @@ -16,9 +16,9 @@ import ( "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/types" "github.com/bytedance/gopkg/util/gopool" "github.com/gin-gonic/gin" diff --git a/relay/channel/aws/adaptor.go b/relay/channel/aws/adaptor.go index 8e8cdd4b..480aea39 100644 --- a/relay/channel/aws/adaptor.go +++ b/relay/channel/aws/adaptor.go @@ -6,12 +6,12 @@ import ( "net/http" "strings" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/claude" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" "github.com/pkg/errors" diff --git a/relay/channel/aws/dto.go b/relay/channel/aws/dto.go index 84facba1..359d2001 100644 --- a/relay/channel/aws/dto.go +++ b/relay/channel/aws/dto.go @@ -8,8 +8,8 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" + "github.com/QuantumNous/new-api/relaykit/dto" ) type AwsClaudeRequest struct { diff --git a/relay/channel/aws/relay-aws.go b/relay/channel/aws/relay-aws.go index a0d9923e..e51927c1 100644 --- a/relay/channel/aws/relay-aws.go +++ b/relay/channel/aws/relay-aws.go @@ -10,13 +10,13 @@ import ( "time" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/claude" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/pkg/errors" diff --git a/relay/channel/baidu/adaptor.go b/relay/channel/baidu/adaptor.go index b8b4735b..fc300a94 100644 --- a/relay/channel/baidu/adaptor.go +++ b/relay/channel/baidu/adaptor.go @@ -7,11 +7,11 @@ import ( "net/http" "strings" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/baidu/dto.go b/relay/channel/baidu/dto.go index 4fa73f89..7fc94090 100644 --- a/relay/channel/baidu/dto.go +++ b/relay/channel/baidu/dto.go @@ -4,7 +4,7 @@ import ( "encoding/json" "time" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" ) type BaiduMessage struct { diff --git a/relay/channel/baidu/relay-baidu.go b/relay/channel/baidu/relay-baidu.go index a76d7689..ab74edc1 100644 --- a/relay/channel/baidu/relay-baidu.go +++ b/relay/channel/baidu/relay-baidu.go @@ -12,11 +12,11 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/samber/lo" "github.com/gin-gonic/gin" diff --git a/relay/channel/baidu_v2/adaptor.go b/relay/channel/baidu_v2/adaptor.go index 94091e38..2a2817ed 100644 --- a/relay/channel/baidu_v2/adaptor.go +++ b/relay/channel/baidu_v2/adaptor.go @@ -7,12 +7,12 @@ import ( "net/http" "strings" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/claude/adaptor.go b/relay/channel/claude/adaptor.go index b8e4a036..bbd711ff 100644 --- a/relay/channel/claude/adaptor.go +++ b/relay/channel/claude/adaptor.go @@ -7,12 +7,12 @@ import ( "net/http" "net/url" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/service/relayconvert" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/claude/message_delta_usage_patch_test.go b/relay/channel/claude/message_delta_usage_patch_test.go index 42c2c70c..d698bf0a 100644 --- a/relay/channel/claude/message_delta_usage_patch_test.go +++ b/relay/channel/claude/message_delta_usage_patch_test.go @@ -3,8 +3,8 @@ package claude import ( "testing" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting/model_setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/relay/channel/claude/relay-claude.go b/relay/channel/claude/relay-claude.go index 8f9c74b3..2f424b32 100644 --- a/relay/channel/claude/relay-claude.go +++ b/relay/channel/claude/relay-claude.go @@ -7,14 +7,14 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/service/relayconvert" "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/claude/relay_claude_test.go b/relay/channel/claude/relay_claude_test.go index ca8191e7..3975658f 100644 --- a/relay/channel/claude/relay_claude_test.go +++ b/relay/channel/claude/relay_claude_test.go @@ -4,8 +4,9 @@ import ( "strings" "testing" - "github.com/QuantumNous/new-api/dto" - "github.com/QuantumNous/new-api/service/relayconvert" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -336,7 +337,7 @@ func TestOpenAIChatRequestToClaudeMessages_ClaudeOpus48HighUsesAdaptiveThinking( }, } - claudeRequest, err := relayconvert.OpenAIChatRequestToClaudeMessages(nil, request) + claudeRequest, err := relayconvert.OpenAIChatRequestToClaudeMessages(nil, &relaycommon.RelayInfo{}, request) require.NoError(t, err) require.Equal(t, "claude-opus-4-8", claudeRequest.Model) require.NotNil(t, claudeRequest.Thinking) @@ -362,7 +363,7 @@ func TestOpenAIChatRequestToClaudeMessages_ClaudeOpus48ThinkingUsesAdaptiveHighE }, } - claudeRequest, err := relayconvert.OpenAIChatRequestToClaudeMessages(nil, request) + claudeRequest, err := relayconvert.OpenAIChatRequestToClaudeMessages(nil, &relaycommon.RelayInfo{}, request) require.NoError(t, err) require.Equal(t, "claude-opus-4-8", claudeRequest.Model) require.NotNil(t, claudeRequest.Thinking) diff --git a/relay/channel/claude/tool_billing_test.go b/relay/channel/claude/tool_billing_test.go index 3a780c7d..61daf42c 100644 --- a/relay/channel/claude/tool_billing_test.go +++ b/relay/channel/claude/tool_billing_test.go @@ -4,10 +4,10 @@ import ( "net/http/httptest" "testing" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/relay/channel/cloudflare/adaptor.go b/relay/channel/cloudflare/adaptor.go index af344623..172db70a 100644 --- a/relay/channel/cloudflare/adaptor.go +++ b/relay/channel/cloudflare/adaptor.go @@ -7,12 +7,12 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/cloudflare/dto.go b/relay/channel/cloudflare/dto.go index 7dcb6722..dc1cb327 100644 --- a/relay/channel/cloudflare/dto.go +++ b/relay/channel/cloudflare/dto.go @@ -1,6 +1,6 @@ package cloudflare -import "github.com/QuantumNous/new-api/dto" +import "github.com/QuantumNous/new-api/relaykit/dto" type CfRequest struct { Messages []dto.Message `json:"messages,omitempty"` diff --git a/relay/channel/cloudflare/relay_cloudflare.go b/relay/channel/cloudflare/relay_cloudflare.go index 589ff126..c9820878 100644 --- a/relay/channel/cloudflare/relay_cloudflare.go +++ b/relay/channel/cloudflare/relay_cloudflare.go @@ -8,12 +8,12 @@ import ( "strings" "time" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/samber/lo" "github.com/gin-gonic/gin" diff --git a/relay/channel/codex/adaptor.go b/relay/channel/codex/adaptor.go index 45ec08d5..e42a253e 100644 --- a/relay/channel/codex/adaptor.go +++ b/relay/channel/codex/adaptor.go @@ -8,12 +8,12 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/cohere/adaptor.go b/relay/channel/cohere/adaptor.go index 664eb678..b6b68f1b 100644 --- a/relay/channel/cohere/adaptor.go +++ b/relay/channel/cohere/adaptor.go @@ -6,11 +6,11 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/cohere/dto.go b/relay/channel/cohere/dto.go index 2ab6385c..12ee1797 100644 --- a/relay/channel/cohere/dto.go +++ b/relay/channel/cohere/dto.go @@ -1,6 +1,6 @@ package cohere -import "github.com/QuantumNous/new-api/dto" +import "github.com/QuantumNous/new-api/relaykit/dto" type CohereRequest struct { Model string `json:"model"` diff --git a/relay/channel/cohere/relay-cohere.go b/relay/channel/cohere/relay-cohere.go index 7b47789f..30a3038c 100644 --- a/relay/channel/cohere/relay-cohere.go +++ b/relay/channel/cohere/relay-cohere.go @@ -8,11 +8,11 @@ import ( "time" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/samber/lo" diff --git a/relay/channel/coze/adaptor.go b/relay/channel/coze/adaptor.go index 30f229a3..7b2d35ea 100644 --- a/relay/channel/coze/adaptor.go +++ b/relay/channel/coze/adaptor.go @@ -8,10 +8,10 @@ import ( "net/http" "time" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/coze/relay-coze.go b/relay/channel/coze/relay-coze.go index 1337a6bd..62eeab85 100644 --- a/relay/channel/coze/relay-coze.go +++ b/relay/channel/coze/relay-coze.go @@ -10,11 +10,11 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/samber/lo" "github.com/gin-gonic/gin" diff --git a/relay/channel/deepseek/adaptor.go b/relay/channel/deepseek/adaptor.go index 60eaf22b..57c4c3ec 100644 --- a/relay/channel/deepseek/adaptor.go +++ b/relay/channel/deepseek/adaptor.go @@ -8,14 +8,14 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/claude" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/reasoning" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/dify/adaptor.go b/relay/channel/dify/adaptor.go index 4ffee3e6..e88436e1 100644 --- a/relay/channel/dify/adaptor.go +++ b/relay/channel/dify/adaptor.go @@ -6,10 +6,10 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/dify/dto.go b/relay/channel/dify/dto.go index b4029a0c..500941d2 100644 --- a/relay/channel/dify/dto.go +++ b/relay/channel/dify/dto.go @@ -1,7 +1,7 @@ package dify import ( - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" ) type DifyChatRequest struct { diff --git a/relay/channel/dify/relay-dify.go b/relay/channel/dify/relay-dify.go index 263ad668..2fcf2f5f 100644 --- a/relay/channel/dify/relay-dify.go +++ b/relay/channel/dify/relay-dify.go @@ -13,11 +13,11 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/samber/lo" "github.com/gin-gonic/gin" diff --git a/relay/channel/gemini/adaptor.go b/relay/channel/gemini/adaptor.go index e0ab48e2..c96ecca6 100644 --- a/relay/channel/gemini/adaptor.go +++ b/relay/channel/gemini/adaptor.go @@ -7,14 +7,14 @@ import ( "net/http" "strings" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/service/relayconvert" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/model_setting" "github.com/QuantumNous/new-api/setting/reasoning" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/samber/lo" diff --git a/relay/channel/gemini/adaptor_responses_test.go b/relay/channel/gemini/adaptor_responses_test.go index a4ab67bc..fb0888d8 100644 --- a/relay/channel/gemini/adaptor_responses_test.go +++ b/relay/channel/gemini/adaptor_responses_test.go @@ -4,8 +4,8 @@ import ( "testing" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" diff --git a/relay/channel/gemini/relay-gemini-native.go b/relay/channel/gemini/relay-gemini-native.go index f3bd7944..74742d66 100644 --- a/relay/channel/gemini/relay-gemini-native.go +++ b/relay/channel/gemini/relay-gemini-native.go @@ -7,12 +7,12 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index 5052c4cc..84acea73 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -11,14 +11,14 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/service/relayconvert" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/gemini/relay_gemini_usage_test.go b/relay/channel/gemini/relay_gemini_usage_test.go index bd4c819d..1ae34c13 100644 --- a/relay/channel/gemini/relay_gemini_usage_test.go +++ b/relay/channel/gemini/relay_gemini_usage_test.go @@ -9,9 +9,9 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) diff --git a/relay/channel/gemini/relay_responses.go b/relay/channel/gemini/relay_responses.go index 5c987962..7b3d746b 100644 --- a/relay/channel/gemini/relay_responses.go +++ b/relay/channel/gemini/relay_responses.go @@ -8,13 +8,13 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/service/relayconvert" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/gemini/relay_responses_test.go b/relay/channel/gemini/relay_responses_test.go index 84fed51d..8484d69a 100644 --- a/relay/channel/gemini/relay_responses_test.go +++ b/relay/channel/gemini/relay_responses_test.go @@ -11,10 +11,10 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/relay/channel/jimeng/adaptor.go b/relay/channel/jimeng/adaptor.go index 1938ac1b..658d9236 100644 --- a/relay/channel/jimeng/adaptor.go +++ b/relay/channel/jimeng/adaptor.go @@ -7,12 +7,12 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/jimeng/image.go b/relay/channel/jimeng/image.go index e422e061..888531bd 100644 --- a/relay/channel/jimeng/image.go +++ b/relay/channel/jimeng/image.go @@ -6,10 +6,10 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/jina/adaptor.go b/relay/channel/jina/adaptor.go index 3f2d01d9..13f8cd7b 100644 --- a/relay/channel/jina/adaptor.go +++ b/relay/channel/jina/adaptor.go @@ -6,13 +6,13 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/common_handler" "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/minimax/adaptor.go b/relay/channel/minimax/adaptor.go index 56d3a1ec..70e0deb7 100644 --- a/relay/channel/minimax/adaptor.go +++ b/relay/channel/minimax/adaptor.go @@ -8,13 +8,13 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/claude" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" "github.com/samber/lo" diff --git a/relay/channel/minimax/adaptor_test.go b/relay/channel/minimax/adaptor_test.go index 46d57c11..41a70423 100644 --- a/relay/channel/minimax/adaptor_test.go +++ b/relay/channel/minimax/adaptor_test.go @@ -8,9 +8,9 @@ import ( "testing" "time" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/minimax/image.go b/relay/channel/minimax/image.go index 9b316bdc..c86cd47d 100644 --- a/relay/channel/minimax/image.go +++ b/relay/channel/minimax/image.go @@ -8,10 +8,10 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/minimax/relay-minimax.go b/relay/channel/minimax/relay-minimax.go index a1a05150..e86efc0f 100644 --- a/relay/channel/minimax/relay-minimax.go +++ b/relay/channel/minimax/relay-minimax.go @@ -6,7 +6,7 @@ import ( channelconstant "github.com/QuantumNous/new-api/constant" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" ) func GetRequestURL(info *relaycommon.RelayInfo) (string, error) { diff --git a/relay/channel/minimax/tts.go b/relay/channel/minimax/tts.go index 61ecabf8..b3968df5 100644 --- a/relay/channel/minimax/tts.go +++ b/relay/channel/minimax/tts.go @@ -9,10 +9,10 @@ import ( "net/http" "strings" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/mistral/adaptor.go b/relay/channel/mistral/adaptor.go index 88d72e0f..fa75b03b 100644 --- a/relay/channel/mistral/adaptor.go +++ b/relay/channel/mistral/adaptor.go @@ -5,11 +5,11 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/mistral/text.go b/relay/channel/mistral/text.go index d43bc36b..d2e6ef42 100644 --- a/relay/channel/mistral/text.go +++ b/relay/channel/mistral/text.go @@ -4,7 +4,7 @@ import ( "regexp" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" ) var mistralToolCallIdRegexp = regexp.MustCompile("^[a-zA-Z0-9]{9}$") diff --git a/relay/channel/mokaai/adaptor.go b/relay/channel/mokaai/adaptor.go index f50c1e6b..3233308f 100644 --- a/relay/channel/mokaai/adaptor.go +++ b/relay/channel/mokaai/adaptor.go @@ -7,11 +7,11 @@ import ( "net/http" "strings" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/mokaai/relay-mokaai.go b/relay/channel/mokaai/relay-mokaai.go index 4949ed64..71780216 100644 --- a/relay/channel/mokaai/relay-mokaai.go +++ b/relay/channel/mokaai/relay-mokaai.go @@ -6,10 +6,10 @@ import ( "net/http" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/moonshot/adaptor.go b/relay/channel/moonshot/adaptor.go index c90ae073..04948647 100644 --- a/relay/channel/moonshot/adaptor.go +++ b/relay/channel/moonshot/adaptor.go @@ -9,13 +9,13 @@ import ( "github.com/QuantumNous/new-api/common" channelconstant "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/claude" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/moonshot/adaptor_test.go b/relay/channel/moonshot/adaptor_test.go index cfa1cb49..2675a92a 100644 --- a/relay/channel/moonshot/adaptor_test.go +++ b/relay/channel/moonshot/adaptor_test.go @@ -4,8 +4,8 @@ import ( "testing" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/stretchr/testify/require" ) diff --git a/relay/channel/ollama/adaptor.go b/relay/channel/ollama/adaptor.go index a3013e2f..998e438f 100644 --- a/relay/channel/ollama/adaptor.go +++ b/relay/channel/ollama/adaptor.go @@ -6,12 +6,12 @@ import ( "net/http" "strings" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/ollama/relay-ollama.go b/relay/channel/ollama/relay-ollama.go index 06e4d94c..874d2e9b 100644 --- a/relay/channel/ollama/relay-ollama.go +++ b/relay/channel/ollama/relay-ollama.go @@ -9,11 +9,11 @@ import ( "time" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/samber/lo" diff --git a/relay/channel/ollama/stream.go b/relay/channel/ollama/stream.go index b57761fa..20e36015 100644 --- a/relay/channel/ollama/stream.go +++ b/relay/channel/ollama/stream.go @@ -10,12 +10,12 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/ollama/stream_test.go b/relay/channel/ollama/stream_test.go index 8d28396c..8ba58b19 100644 --- a/relay/channel/ollama/stream_test.go +++ b/relay/channel/ollama/stream_test.go @@ -9,8 +9,8 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index 3fd24665..4f1c4286 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -15,11 +15,11 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/ai360" "github.com/QuantumNous/new-api/relay/channel/lingyiwanwu" + "github.com/QuantumNous/new-api/relaykit/dto" //"github.com/QuantumNous/new-api/relay/channel/minimax" "github.com/QuantumNous/new-api/relay/channel/openrouter" @@ -27,10 +27,10 @@ import ( relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/common_handler" relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/model_setting" "github.com/QuantumNous/new-api/setting/reasoning" - "github.com/QuantumNous/new-api/types" "github.com/samber/lo" "github.com/gin-gonic/gin" diff --git a/relay/channel/openai/audio.go b/relay/channel/openai/audio.go index f18819e8..f17d918a 100644 --- a/relay/channel/openai/audio.go +++ b/relay/channel/openai/audio.go @@ -9,12 +9,12 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/openai/chat_via_responses.go b/relay/channel/openai/chat_via_responses.go index 18758e72..25caeb58 100644 --- a/relay/channel/openai/chat_via_responses.go +++ b/relay/channel/openai/chat_via_responses.go @@ -9,13 +9,13 @@ import ( "time" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/service/relayconvert" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/openai/chat_via_responses_test.go b/relay/channel/openai/chat_via_responses_test.go index dd6d8e39..2bf98b56 100644 --- a/relay/channel/openai/chat_via_responses_test.go +++ b/relay/channel/openai/chat_via_responses_test.go @@ -10,7 +10,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) diff --git a/relay/channel/openai/helper.go b/relay/channel/openai/helper.go index 840708eb..666235ff 100644 --- a/relay/channel/openai/helper.go +++ b/relay/channel/openai/helper.go @@ -5,14 +5,14 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/service/relayconvert" - "github.com/QuantumNous/new-api/types" "github.com/samber/lo" diff --git a/relay/channel/openai/image_edit_test.go b/relay/channel/openai/image_edit_test.go index 857ab243..0d1a9d85 100644 --- a/relay/channel/openai/image_edit_test.go +++ b/relay/channel/openai/image_edit_test.go @@ -9,9 +9,9 @@ import ( "testing" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) diff --git a/relay/channel/openai/relay-openai.go b/relay/channel/openai/relay-openai.go index 17cad566..9a0619eb 100644 --- a/relay/channel/openai/relay-openai.go +++ b/relay/channel/openai/relay-openai.go @@ -8,14 +8,14 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/relay/channel/openrouter" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/service/relayconvert" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/openai/relay_image.go b/relay/channel/openai/relay_image.go index e0f09aae..1e6be0dd 100644 --- a/relay/channel/openai/relay_image.go +++ b/relay/channel/openai/relay_image.go @@ -10,12 +10,12 @@ import ( "time" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/tidwall/gjson" diff --git a/relay/channel/openai/relay_realtime.go b/relay/channel/openai/relay_realtime.go index bb5c3587..bea97780 100644 --- a/relay/channel/openai/relay_realtime.go +++ b/relay/channel/openai/relay_realtime.go @@ -4,12 +4,12 @@ import ( "fmt" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/bytedance/gopkg/util/gopool" "github.com/gin-gonic/gin" diff --git a/relay/channel/openai/relay_responses.go b/relay/channel/openai/relay_responses.go index 697cc965..ceca1af3 100644 --- a/relay/channel/openai/relay_responses.go +++ b/relay/channel/openai/relay_responses.go @@ -7,12 +7,12 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/openai/relay_responses_billing_test.go b/relay/channel/openai/relay_responses_billing_test.go index febe69c4..0707ddf5 100644 --- a/relay/channel/openai/relay_responses_billing_test.go +++ b/relay/channel/openai/relay_responses_billing_test.go @@ -10,8 +10,8 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" diff --git a/relay/channel/openai/relay_responses_compact.go b/relay/channel/openai/relay_responses_compact.go index 1180538c..ff30d36e 100644 --- a/relay/channel/openai/relay_responses_compact.go +++ b/relay/channel/openai/relay_responses_compact.go @@ -5,9 +5,9 @@ import ( "net/http" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/openai/responses_via_chat.go b/relay/channel/openai/responses_via_chat.go index 549479d7..53b9d33c 100644 --- a/relay/channel/openai/responses_via_chat.go +++ b/relay/channel/openai/responses_via_chat.go @@ -6,13 +6,13 @@ import ( "net/http" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/service/relayconvert" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/openai/usage.go b/relay/channel/openai/usage.go index 4085a1f3..cd43f535 100644 --- a/relay/channel/openai/usage.go +++ b/relay/channel/openai/usage.go @@ -3,8 +3,8 @@ package openai import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" ) func applyUsagePostProcessing(info *relaycommon.RelayInfo, usage *dto.Usage, responseBody []byte) { diff --git a/relay/channel/palm/adaptor.go b/relay/channel/palm/adaptor.go index 3c1302d8..a50c979b 100644 --- a/relay/channel/palm/adaptor.go +++ b/relay/channel/palm/adaptor.go @@ -6,11 +6,11 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/palm/dto.go b/relay/channel/palm/dto.go index 47ca3fc6..66b86a78 100644 --- a/relay/channel/palm/dto.go +++ b/relay/channel/palm/dto.go @@ -1,6 +1,6 @@ package palm -import "github.com/QuantumNous/new-api/dto" +import "github.com/QuantumNous/new-api/relaykit/dto" type PaLMChatMessage struct { Author string `json:"author"` diff --git a/relay/channel/palm/relay-palm.go b/relay/channel/palm/relay-palm.go index 786ea4cd..89ac14c7 100644 --- a/relay/channel/palm/relay-palm.go +++ b/relay/channel/palm/relay-palm.go @@ -7,11 +7,11 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/perplexity/adaptor.go b/relay/channel/perplexity/adaptor.go index 6b036909..886f541d 100644 --- a/relay/channel/perplexity/adaptor.go +++ b/relay/channel/perplexity/adaptor.go @@ -6,12 +6,12 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/samber/lo" "github.com/gin-gonic/gin" diff --git a/relay/channel/perplexity/relay-perplexity.go b/relay/channel/perplexity/relay-perplexity.go index 4f5767e3..0dfa8cc4 100644 --- a/relay/channel/perplexity/relay-perplexity.go +++ b/relay/channel/perplexity/relay-perplexity.go @@ -1,6 +1,6 @@ package perplexity -import "github.com/QuantumNous/new-api/dto" +import "github.com/QuantumNous/new-api/relaykit/dto" func requestOpenAI2Perplexity(request dto.GeneralOpenAIRequest) *dto.GeneralOpenAIRequest { messages := make([]dto.Message, 0, len(request.Messages)) diff --git a/relay/channel/replicate/adaptor.go b/relay/channel/replicate/adaptor.go index 67350205..fa548321 100644 --- a/relay/channel/replicate/adaptor.go +++ b/relay/channel/replicate/adaptor.go @@ -14,12 +14,12 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/samber/lo" diff --git a/relay/channel/siliconflow/adaptor.go b/relay/channel/siliconflow/adaptor.go index 3e9bee55..fd3376c0 100644 --- a/relay/channel/siliconflow/adaptor.go +++ b/relay/channel/siliconflow/adaptor.go @@ -7,12 +7,12 @@ import ( "net/http" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" "github.com/samber/lo" diff --git a/relay/channel/siliconflow/dto.go b/relay/channel/siliconflow/dto.go index 10097510..3f675c2e 100644 --- a/relay/channel/siliconflow/dto.go +++ b/relay/channel/siliconflow/dto.go @@ -1,6 +1,6 @@ package siliconflow -import "github.com/QuantumNous/new-api/dto" +import "github.com/QuantumNous/new-api/relaykit/dto" type SFTokens struct { InputTokens int `json:"input_tokens"` diff --git a/relay/channel/siliconflow/relay-siliconflow.go b/relay/channel/siliconflow/relay-siliconflow.go index 421731fb..35079eac 100644 --- a/relay/channel/siliconflow/relay-siliconflow.go +++ b/relay/channel/siliconflow/relay-siliconflow.go @@ -5,10 +5,10 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/submodel/adaptor.go b/relay/channel/submodel/adaptor.go index 58b2a3b2..10ee770b 100644 --- a/relay/channel/submodel/adaptor.go +++ b/relay/channel/submodel/adaptor.go @@ -5,11 +5,11 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/task/ali/adaptor.go b/relay/channel/task/ali/adaptor.go index 51ffa743..7452c614 100644 --- a/relay/channel/task/ali/adaptor.go +++ b/relay/channel/task/ali/adaptor.go @@ -9,12 +9,13 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" + taskdto "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/service" "github.com/samber/lo" @@ -129,7 +130,7 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { a.apiKey = info.ApiKey } -func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) { +func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *taskdto.TaskError) { // ValidateMultipartDirect 负责解析并将原始 TaskSubmitReq 存入 context return relaycommon.ValidateMultipartDirect(c, info) } @@ -477,7 +478,7 @@ func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, req } // DoResponse handles upstream response -func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { +func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *taskdto.TaskError) { responseBody, err := io.ReadAll(resp.Body) if err != nil { taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) diff --git a/relay/channel/task/doubao/adaptor.go b/relay/channel/task/doubao/adaptor.go index e826cd71..69302a67 100644 --- a/relay/channel/task/doubao/adaptor.go +++ b/relay/channel/task/doubao/adaptor.go @@ -11,11 +11,12 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + taskdto "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/service" "github.com/gin-gonic/gin" @@ -116,7 +117,7 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { } // ValidateRequestAndSetAction parses body, validates fields and sets default action. -func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) { +func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *taskdto.TaskError) { // Accept only POST /v1/video/generations as "generate" action. return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate) } @@ -207,7 +208,7 @@ func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, req } // DoResponse handles upstream response, returns taskID etc. -func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { +func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *taskdto.TaskError) { responseBody, err := io.ReadAll(resp.Body) if err != nil { taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) diff --git a/relay/channel/task/gemini/adaptor.go b/relay/channel/task/gemini/adaptor.go index 48aa0631..2b1bf8ed 100644 --- a/relay/channel/task/gemini/adaptor.go +++ b/relay/channel/task/gemini/adaptor.go @@ -11,11 +11,12 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + taskdto "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/relay/channel" taskcommon "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/model_setting" "github.com/gin-gonic/gin" @@ -40,7 +41,7 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { } // ValidateRequestAndSetAction parses body, validates fields and sets default action. -func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) { +func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *taskdto.TaskError) { return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionTextGenerate) } @@ -120,7 +121,7 @@ func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, req } // DoResponse handles upstream response, returns taskID etc. -func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { +func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *taskdto.TaskError) { responseBody, err := io.ReadAll(resp.Body) if err != nil { return "", nil, service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) diff --git a/relay/channel/task/hailuo/adaptor.go b/relay/channel/task/hailuo/adaptor.go index 28b3a97f..af9f5c57 100644 --- a/relay/channel/task/hailuo/adaptor.go +++ b/relay/channel/task/hailuo/adaptor.go @@ -15,10 +15,11 @@ import ( "github.com/pkg/errors" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + taskdto "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" taskcommon "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/service" ) @@ -36,7 +37,7 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { a.apiKey = info.ApiKey } -func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) { +func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *taskdto.TaskError) { return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate) } @@ -78,7 +79,7 @@ func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, req return channel.DoTaskApiRequest(a, c, info, requestBody) } -func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { +func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *taskdto.TaskError) { responseBody, err := io.ReadAll(resp.Body) if err != nil { taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) diff --git a/relay/channel/task/jimeng/adaptor.go b/relay/channel/task/jimeng/adaptor.go index e6211b1e..5e788d34 100644 --- a/relay/channel/task/jimeng/adaptor.go +++ b/relay/channel/task/jimeng/adaptor.go @@ -22,10 +22,11 @@ import ( "github.com/pkg/errors" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + taskdto "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" taskcommon "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/service" ) @@ -97,7 +98,7 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { } // ValidateRequestAndSetAction parses body, validates fields and sets default action. -func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) { +func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *taskdto.TaskError) { return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate) } @@ -182,7 +183,7 @@ func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, req } // DoResponse handles upstream response, returns taskID etc. -func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { +func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *taskdto.TaskError) { responseBody, err := io.ReadAll(resp.Body) if err != nil { taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) diff --git a/relay/channel/task/kling/adaptor.go b/relay/channel/task/kling/adaptor.go index c7a492b9..200c3c68 100644 --- a/relay/channel/task/kling/adaptor.go +++ b/relay/channel/task/kling/adaptor.go @@ -20,10 +20,11 @@ import ( "github.com/pkg/errors" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + taskdto "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" taskcommon "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/service" ) @@ -127,7 +128,7 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { } // ValidateRequestAndSetAction parses body, validates fields and sets default action. -func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) { +func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *taskdto.TaskError) { // Use the standard validation method for TaskSubmitReq return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate) } @@ -188,7 +189,7 @@ func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, req } // DoResponse handles upstream response, returns taskID etc. -func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { +func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *taskdto.TaskError) { responseBody, err := io.ReadAll(resp.Body) if err != nil { taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) diff --git a/relay/channel/task/vertex/adaptor.go b/relay/channel/task/vertex/adaptor.go index a296d4cc..d73c151c 100644 --- a/relay/channel/task/vertex/adaptor.go +++ b/relay/channel/task/vertex/adaptor.go @@ -14,12 +14,13 @@ import ( "github.com/gin-gonic/gin" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + taskdto "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" geminitask "github.com/QuantumNous/new-api/relay/channel/task/gemini" taskcommon "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" vertexcore "github.com/QuantumNous/new-api/relay/channel/vertex" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/service" ) @@ -75,7 +76,7 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { } // ValidateRequestAndSetAction parses body, validates fields and sets default action. -func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) { +func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *taskdto.TaskError) { // Use the standard validation method for TaskSubmitReq return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionTextGenerate) } @@ -191,7 +192,7 @@ func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, req } // DoResponse handles upstream response, returns taskID etc. -func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { +func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *taskdto.TaskError) { responseBody, err := io.ReadAll(resp.Body) if err != nil { return "", nil, service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) diff --git a/relay/channel/task/vidu/adaptor.go b/relay/channel/task/vidu/adaptor.go index 6ae1c181..62e029bb 100644 --- a/relay/channel/task/vidu/adaptor.go +++ b/relay/channel/task/vidu/adaptor.go @@ -12,11 +12,12 @@ import ( "github.com/gin-gonic/gin" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + taskdto "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/relay/channel" taskcommon "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/service" "github.com/pkg/errors" @@ -83,7 +84,7 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { a.baseURL = info.ChannelBaseUrl } -func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) *dto.TaskError { +func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) *taskdto.TaskError { if err := relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate); err != nil { return err } @@ -161,7 +162,7 @@ func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, req return channel.DoTaskApiRequest(a, c, info, requestBody) } -func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { +func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *taskdto.TaskError) { responseBody, err := io.ReadAll(resp.Body) if err != nil { taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) diff --git a/relay/channel/tencent/adaptor.go b/relay/channel/tencent/adaptor.go index eb698553..69221b55 100644 --- a/relay/channel/tencent/adaptor.go +++ b/relay/channel/tencent/adaptor.go @@ -10,10 +10,10 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/tencent/relay-tencent.go b/relay/channel/tencent/relay-tencent.go index 4cda7541..611c186d 100644 --- a/relay/channel/tencent/relay-tencent.go +++ b/relay/channel/tencent/relay-tencent.go @@ -16,11 +16,11 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/vertex/adaptor.go b/relay/channel/vertex/adaptor.go index 3145fce2..c60d75d2 100644 --- a/relay/channel/vertex/adaptor.go +++ b/relay/channel/vertex/adaptor.go @@ -8,17 +8,17 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/claude" "github.com/QuantumNous/new-api/relay/channel/gemini" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/model_setting" "github.com/QuantumNous/new-api/setting/reasoning" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/samber/lo" diff --git a/relay/channel/vertex/dto.go b/relay/channel/vertex/dto.go index c1d13a6d..7beab9ed 100644 --- a/relay/channel/vertex/dto.go +++ b/relay/channel/vertex/dto.go @@ -3,7 +3,7 @@ package vertex import ( "encoding/json" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" ) type VertexAIClaudeRequest struct { diff --git a/relay/channel/volcengine/adaptor.go b/relay/channel/volcengine/adaptor.go index ba9f223b..59894936 100644 --- a/relay/channel/volcengine/adaptor.go +++ b/relay/channel/volcengine/adaptor.go @@ -11,14 +11,14 @@ import ( "strings" channelconstant "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/claude" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/samber/lo" diff --git a/relay/channel/volcengine/tts.go b/relay/channel/volcengine/tts.go index 2b03981d..6ea28275 100644 --- a/relay/channel/volcengine/tts.go +++ b/relay/channel/volcengine/tts.go @@ -10,9 +10,9 @@ import ( "net/http" "strings" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/gorilla/websocket" diff --git a/relay/channel/xai/adaptor.go b/relay/channel/xai/adaptor.go index 64f622f4..0db042f0 100644 --- a/relay/channel/xai/adaptor.go +++ b/relay/channel/xai/adaptor.go @@ -6,11 +6,11 @@ import ( "net/http" "strings" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/relay/constant" diff --git a/relay/channel/xai/dto.go b/relay/channel/xai/dto.go index 371d62a4..4cb8d52e 100644 --- a/relay/channel/xai/dto.go +++ b/relay/channel/xai/dto.go @@ -1,6 +1,6 @@ package xai -import "github.com/QuantumNous/new-api/dto" +import "github.com/QuantumNous/new-api/relaykit/dto" // ChatCompletionResponse represents the response from XAI chat completion API type ChatCompletionResponse struct { diff --git a/relay/channel/xai/text.go b/relay/channel/xai/text.go index f9a8ee2e..071b0969 100644 --- a/relay/channel/xai/text.go +++ b/relay/channel/xai/text.go @@ -6,12 +6,12 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/xunfei/adaptor.go b/relay/channel/xunfei/adaptor.go index 686b0cbd..d92029e2 100644 --- a/relay/channel/xunfei/adaptor.go +++ b/relay/channel/xunfei/adaptor.go @@ -6,10 +6,10 @@ import ( "net/http" "strings" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/xunfei/dto.go b/relay/channel/xunfei/dto.go index 71a40f2d..767c9d2b 100644 --- a/relay/channel/xunfei/dto.go +++ b/relay/channel/xunfei/dto.go @@ -1,6 +1,6 @@ package xunfei -import "github.com/QuantumNous/new-api/dto" +import "github.com/QuantumNous/new-api/relaykit/dto" type XunfeiMessage struct { Role string `json:"role"` diff --git a/relay/channel/xunfei/relay-xunfei.go b/relay/channel/xunfei/relay-xunfei.go index 70fde810..fc80ba77 100644 --- a/relay/channel/xunfei/relay-xunfei.go +++ b/relay/channel/xunfei/relay-xunfei.go @@ -13,9 +13,9 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/helper" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/samber/lo" "github.com/gin-gonic/gin" diff --git a/relay/channel/zhipu/adaptor.go b/relay/channel/zhipu/adaptor.go index 3ed4b359..d740fc5f 100644 --- a/relay/channel/zhipu/adaptor.go +++ b/relay/channel/zhipu/adaptor.go @@ -6,10 +6,10 @@ import ( "io" "net/http" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/samber/lo" "github.com/gin-gonic/gin" diff --git a/relay/channel/zhipu/dto.go b/relay/channel/zhipu/dto.go index 5ca91362..cbfc2b8f 100644 --- a/relay/channel/zhipu/dto.go +++ b/relay/channel/zhipu/dto.go @@ -3,7 +3,7 @@ package zhipu import ( "time" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" ) type ZhipuMessage struct { diff --git a/relay/channel/zhipu/relay-zhipu.go b/relay/channel/zhipu/relay-zhipu.go index 6754c02c..0c280e2b 100644 --- a/relay/channel/zhipu/relay-zhipu.go +++ b/relay/channel/zhipu/relay-zhipu.go @@ -11,11 +11,11 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/samber/lo" "github.com/gin-gonic/gin" diff --git a/relay/channel/zhipu_4v/adaptor.go b/relay/channel/zhipu_4v/adaptor.go index 0af8a16b..04459015 100644 --- a/relay/channel/zhipu_4v/adaptor.go +++ b/relay/channel/zhipu_4v/adaptor.go @@ -7,13 +7,13 @@ import ( "net/http" channelconstant "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/claude" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/samber/lo" "github.com/gin-gonic/gin" diff --git a/relay/channel/zhipu_4v/dto.go b/relay/channel/zhipu_4v/dto.go index e96feda6..2a012993 100644 --- a/relay/channel/zhipu_4v/dto.go +++ b/relay/channel/zhipu_4v/dto.go @@ -3,8 +3,8 @@ package zhipu_4v import ( "time" - "github.com/QuantumNous/new-api/dto" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" ) // type ZhipuMessage struct { diff --git a/relay/channel/zhipu_4v/image.go b/relay/channel/zhipu_4v/image.go index b1fd2c8e..cdb35a82 100644 --- a/relay/channel/zhipu_4v/image.go +++ b/relay/channel/zhipu_4v/image.go @@ -5,11 +5,11 @@ import ( "net/http" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/zhipu_4v/relay-zhipu_v4.go b/relay/channel/zhipu_4v/relay-zhipu_v4.go index 91ef0c47..055f899e 100644 --- a/relay/channel/zhipu_4v/relay-zhipu_v4.go +++ b/relay/channel/zhipu_4v/relay-zhipu_v4.go @@ -3,7 +3,7 @@ package zhipu_4v import ( "strings" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" ) func requestOpenAI2Zhipu(request dto.GeneralOpenAIRequest) *dto.GeneralOpenAIRequest { diff --git a/relay/chat_completions_via_responses.go b/relay/chat_completions_via_responses.go index 3d44abbb..ccc7fb28 100644 --- a/relay/chat_completions_via_responses.go +++ b/relay/chat_completions_via_responses.go @@ -8,13 +8,13 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" openaichannel "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/chat_completions_via_responses_test.go b/relay/chat_completions_via_responses_test.go index 18587874..9f4645a7 100644 --- a/relay/chat_completions_via_responses_test.go +++ b/relay/chat_completions_via_responses_test.go @@ -5,7 +5,7 @@ import ( "testing" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/relay/claude_handler.go b/relay/claude_handler.go index e4a49203..63bc11f5 100644 --- a/relay/claude_handler.go +++ b/relay/claude_handler.go @@ -9,14 +9,14 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/model_setting" "github.com/QuantumNous/new-api/setting/reasoning" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/common/override.go b/relay/common/override.go index db59482a..f92daf77 100644 --- a/relay/common/override.go +++ b/relay/common/override.go @@ -10,7 +10,7 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/samber/lo" "github.com/tidwall/gjson" "github.com/tidwall/sjson" diff --git a/relay/common/override_test.go b/relay/common/override_test.go index 79688113..c5fc7f5d 100644 --- a/relay/common/override_test.go +++ b/relay/common/override_test.go @@ -7,9 +7,9 @@ import ( "testing" common2 "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting/model_setting" "github.com/samber/lo" "github.com/stretchr/testify/require" diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index eaf488e0..b26fd799 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -10,11 +10,12 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/pkg/billingexpr" relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" @@ -28,22 +29,15 @@ type ThinkingContentInfo struct { } const ( - LastMessageTypeNone = "none" - LastMessageTypeText = "text" - LastMessageTypeTools = "tools" - LastMessageTypeThinking = "thinking" + LastMessageTypeNone = convmeta.LastMessageTypeNone + LastMessageTypeText = convmeta.LastMessageTypeText + LastMessageTypeTools = convmeta.LastMessageTypeTools + LastMessageTypeThinking = convmeta.LastMessageTypeThinking ) -type ClaudeConvertInfo struct { - LastMessagesType string - Index int - Usage *dto.Usage - FinishReason string - Done bool - - ToolCallBaseIndex int - ToolCallMaxIndexOffset int -} +// ClaudeConvertInfo now lives with the converters (convmeta); the alias keeps +// host code and adaptors compiling unchanged. +type ClaudeConvertInfo = convmeta.ClaudeConvertInfo type RerankerInfo struct { Documents []any @@ -184,6 +178,9 @@ type RelayInfo struct { StreamStatus *StreamStatus + // convOptions caches the converter settings snapshot (see ConvOptions). + convOptions *convmeta.Options + ThinkingContentInfo TokenCountMeta *ClaudeConvertInfo @@ -239,6 +236,10 @@ func (info *RelayInfo) InitChannelMeta(c *gin.Context) { info.ChannelMeta = channelMeta + // Channel identity feeds the converter options snapshot (e.g. + // OpenRouterDialect); drop the cache so a cross-channel retry rebuilds it. + info.convOptions = nil + // reset some fields based on channel meta // 重置某些字段,例如模型名称等 if info.Request != nil { @@ -459,7 +460,7 @@ func genBaseRelayInfo(c *gin.Context, request dto.Request) *RelayInfo { isStream := false if request != nil { - isStream = request.IsStream(c) + isStream = request.IsStream(c.Request) } c.Set(string(constant.ContextKeyIsStream), isStream) @@ -679,13 +680,133 @@ func GenRelayInfoAlphaSearch(c *gin.Context, request *dto.AlphaSearchRequest) *R //} func (info *RelayInfo) SetEstimatePromptTokens(promptTokens int) { + if info == nil { + return + } info.estimatePromptTokens = promptTokens } func (info *RelayInfo) GetEstimatePromptTokens() int { + if info == nil { + return 0 + } return info.estimatePromptTokens } +// --------------------------------------------------------------------------- +// convmeta.Meta implementation — the view format converters see. Keep these +// thin: they only expose protocol state, never billing/user fields. +// --------------------------------------------------------------------------- + +var _ convmeta.Meta = (*RelayInfo)(nil) + +func (info *RelayInfo) GetOriginModelName() string { + if info == nil { + return "" + } + return info.OriginModelName +} + +func (info *RelayInfo) GetUpstreamModelName() string { + if info == nil || info.ChannelMeta == nil { + return "" + } + return info.UpstreamModelName +} + +func (info *RelayInfo) HasChannelMeta() bool { return info != nil && info.ChannelMeta != nil } + +func (info *RelayInfo) GetChannelID() int { + if info == nil || info.ChannelMeta == nil { + return 0 + } + return info.ChannelId +} + +func (info *RelayInfo) GetChannelType() int { + if info == nil || info.ChannelMeta == nil { + return 0 + } + return info.ChannelType +} + +func (info *RelayInfo) GetIsStream() bool { + return info != nil && info.IsStream +} + +func (info *RelayInfo) GetReasoningEffort() string { + if info == nil { + return "" + } + return info.ReasoningEffort +} + +func (info *RelayInfo) SetReasoningEffort(effort string) { + if info == nil { + return + } + info.ReasoningEffort = effort +} + +func (info *RelayInfo) EnsureClaudeConvertInfo() *convmeta.ClaudeConvertInfo { + if info == nil { + return &convmeta.ClaudeConvertInfo{ + LastMessagesType: convmeta.LastMessageTypeNone, + } + } + if info.ClaudeConvertInfo == nil { + info.ClaudeConvertInfo = &convmeta.ClaudeConvertInfo{ + LastMessagesType: convmeta.LastMessageTypeNone, + } + } + return info.ClaudeConvertInfo +} + +func (info *RelayInfo) GetSendResponseCount() int { + if info == nil { + return 0 + } + return info.SendResponseCount +} + +func (info *RelayInfo) IncrSendResponseCount() { + if info == nil { + return + } + info.SendResponseCount++ +} + +// ConvOptions snapshots host settings for the converters. Rebuilt on each +// call site's first use; cached so one relay session sees one snapshot. +func (info *RelayInfo) ConvOptions() *convmeta.Options { + if info != nil && info.convOptions != nil { + return info.convOptions + } + + claudeSettings := model_setting.GetClaudeSettings() + geminiSettings := model_setting.GetGeminiSettings() + options := &convmeta.Options{ + Claude: convmeta.ClaudeOptions{ + ThinkingAdapterEnabled: claudeSettings.ThinkingAdapterEnabled, + ThinkingAdapterBudgetTokensPercentage: claudeSettings.ThinkingAdapterBudgetTokensPercentage, + DefaultMaxTokens: claudeSettings.GetDefaultMaxTokens, + }, + Gemini: convmeta.GeminiOptions{ + ThinkingAdapterEnabled: geminiSettings.ThinkingAdapterEnabled, + ThinkingAdapterBudgetTokensPercentage: geminiSettings.ThinkingAdapterBudgetTokensPercentage, + FunctionCallThoughtSignatureEnabled: geminiSettings.FunctionCallThoughtSignatureEnabled, + SupportsImagine: model_setting.IsGeminiModelSupportImagine, + SafetySetting: model_setting.GetGeminiSafetySetting, + }, + OpenRouterDialect: info != nil && info.GetChannelType() == constant.ChannelTypeOpenRouter, + PreserveThinkingSuffix: model_setting.ShouldPreserveThinkingSuffix, + } + if info != nil { + info.convOptions = options + } + return options +} + func (info *RelayInfo) SetFirstResponseTime() { if info.isFirstResponse { info.FirstResponseTime = time.Now() diff --git a/relay/common/relay_info_gemini_settings_test.go b/relay/common/relay_info_gemini_settings_test.go new file mode 100644 index 00000000..1d7f2909 --- /dev/null +++ b/relay/common/relay_info_gemini_settings_test.go @@ -0,0 +1,25 @@ +package common + +import ( + "testing" + + "github.com/QuantumNous/new-api/setting/model_setting" + "github.com/stretchr/testify/assert" +) + +func TestRelayInfoConvOptionsUsesNormalizedGeminiSafetySettings(t *testing.T) { + settings := model_setting.GetGeminiSettings() + original := settings.SafetySettings + t.Cleanup(func() { + settings.SafetySettings = original + }) + settings.SafetySettings = map[string]string{ + "HARM_CATEGORY_HATE_SPEECH": "", + "HARM_CATEGORY_DANGEROUS_CONTENT": "BLOCK_ONLY_HIGH", + } + + options := (&RelayInfo{}).ConvOptions() + + assert.Equal(t, "OFF", options.Gemini.SafetySetting("HARM_CATEGORY_HATE_SPEECH")) + assert.Equal(t, "BLOCK_ONLY_HIGH", options.Gemini.SafetySetting("HARM_CATEGORY_DANGEROUS_CONTENT")) +} diff --git a/relay/common/relay_info_test.go b/relay/common/relay_info_test.go index e53ec804..9e58f3f9 100644 --- a/relay/common/relay_info_test.go +++ b/relay/common/relay_info_test.go @@ -3,7 +3,9 @@ package common import ( "testing" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + "github.com/QuantumNous/new-api/relaykit/types" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -38,3 +40,41 @@ func TestRelayInfoGetFinalRequestRelayFormatNilReceiver(t *testing.T) { var info *RelayInfo require.Equal(t, types.RelayFormat(""), info.GetFinalRequestRelayFormat()) } + +func TestRelayInfoMetaTypedNilReceiver(t *testing.T) { + var info *RelayInfo + var meta convmeta.Meta = info + + assert.Empty(t, meta.GetOriginModelName()) + assert.Empty(t, meta.GetUpstreamModelName()) + assert.False(t, meta.HasChannelMeta()) + assert.Zero(t, meta.GetChannelID()) + assert.Zero(t, meta.GetChannelType()) + assert.False(t, meta.GetIsStream()) + assert.Empty(t, meta.GetReasoningEffort()) + assert.Zero(t, meta.GetEstimatePromptTokens()) + assert.Zero(t, meta.GetSendResponseCount()) + + assert.NotPanics(t, func() { + meta.SetReasoningEffort("high") + meta.IncrSendResponseCount() + meta.AppendRequestConversion(types.RelayFormatClaude) + }) + + firstState := meta.EnsureClaudeConvertInfo() + secondState := meta.EnsureClaudeConvertInfo() + require.NotNil(t, firstState) + require.NotNil(t, secondState) + assert.Equal(t, convmeta.LastMessageTypeNone, firstState.LastMessagesType) + assert.NotSame(t, firstState, secondState) + + firstOptions := meta.ConvOptions() + secondOptions := meta.ConvOptions() + require.NotNil(t, firstOptions) + require.NotNil(t, secondOptions) + assert.NotSame(t, firstOptions, secondOptions) + assert.NotNil(t, firstOptions.Claude.DefaultMaxTokens) + assert.NotNil(t, firstOptions.Gemini.SupportsImagine) + assert.NotNil(t, firstOptions.Gemini.SafetySetting) + assert.NotNil(t, firstOptions.PreserveThinkingSuffix) +} diff --git a/relay/common/request_conversion.go b/relay/common/request_conversion.go index 96b728d2..4a043fcc 100644 --- a/relay/common/request_conversion.go +++ b/relay/common/request_conversion.go @@ -1,31 +1,14 @@ package common import ( - "github.com/QuantumNous/new-api/dto" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + "github.com/QuantumNous/new-api/relaykit/types" ) +// GuessRelayFormatFromRequest moved to convmeta with the converters; the +// delegation keeps host callers unchanged. func GuessRelayFormatFromRequest(req any) (types.RelayFormat, bool) { - switch req.(type) { - case *dto.GeneralOpenAIRequest, dto.GeneralOpenAIRequest: - return types.RelayFormatOpenAI, true - case *dto.OpenAIResponsesRequest, dto.OpenAIResponsesRequest: - return types.RelayFormatOpenAIResponses, true - case *dto.ClaudeRequest, dto.ClaudeRequest: - return types.RelayFormatClaude, true - case *dto.GeminiChatRequest, dto.GeminiChatRequest: - return types.RelayFormatGemini, true - case *dto.EmbeddingRequest, dto.EmbeddingRequest: - return types.RelayFormatEmbedding, true - case *dto.RerankRequest, dto.RerankRequest: - return types.RelayFormatRerank, true - case *dto.ImageRequest, dto.ImageRequest: - return types.RelayFormatOpenAIImage, true - case *dto.AudioRequest, dto.AudioRequest: - return types.RelayFormatOpenAIAudio, true - default: - return "", false - } + return convmeta.GuessRelayFormatFromRequest(req) } func AppendRequestConversionFromRequest(info *RelayInfo, req any) { diff --git a/relay/common/stream_status.go b/relay/common/stream_status.go index 57b0bb97..4734b264 100644 --- a/relay/common/stream_status.go +++ b/relay/common/stream_status.go @@ -29,9 +29,9 @@ type StreamErrorEntry struct { } type StreamStatus struct { - EndReason StreamEndReason - EndError error - endOnce sync.Once + EndReason StreamEndReason + EndError error + endOnce sync.Once mu sync.Mutex Errors []StreamErrorEntry diff --git a/relay/common/tool_usage.go b/relay/common/tool_usage.go index c53c5c54..e9bed625 100644 --- a/relay/common/tool_usage.go +++ b/relay/common/tool_usage.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting/operation_setting" ) diff --git a/relay/common/tool_usage_test.go b/relay/common/tool_usage_test.go index 1ef92d7c..c9776340 100644 --- a/relay/common/tool_usage_test.go +++ b/relay/common/tool_usage_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/relay/common_handler/rerank.go b/relay/common_handler/rerank.go index a3f30ae9..1e7658da 100644 --- a/relay/common_handler/rerank.go +++ b/relay/common_handler/rerank.go @@ -6,12 +6,12 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/relay/channel/xinference" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/compatible_handler.go b/relay/compatible_handler.go index a68cfe73..b1e25e03 100644 --- a/relay/compatible_handler.go +++ b/relay/compatible_handler.go @@ -8,15 +8,15 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/model_setting" "github.com/QuantumNous/new-api/setting/ratio_setting" - "github.com/QuantumNous/new-api/types" "github.com/samber/lo" "github.com/gin-gonic/gin" diff --git a/relay/embedding_handler.go b/relay/embedding_handler.go index a12ef8d3..3be543bd 100644 --- a/relay/embedding_handler.go +++ b/relay/embedding_handler.go @@ -6,12 +6,12 @@ import ( "net/http" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/gemini_handler.go b/relay/gemini_handler.go index 2130c400..786feb8b 100644 --- a/relay/gemini_handler.go +++ b/relay/gemini_handler.go @@ -8,14 +8,14 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/service/relayconvert" "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/helper/billing_expr_request.go b/relay/helper/billing_expr_request.go index 28a44bc8..d83fc6a0 100644 --- a/relay/helper/billing_expr_request.go +++ b/relay/helper/billing_expr_request.go @@ -4,9 +4,9 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/pkg/billingexpr" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/gin-gonic/gin" ) diff --git a/relay/helper/billing_expr_request_test.go b/relay/helper/billing_expr_request_test.go index 9193f4b4..5bc1d6e9 100644 --- a/relay/helper/billing_expr_request_test.go +++ b/relay/helper/billing_expr_request_test.go @@ -8,8 +8,8 @@ import ( "testing" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/gin-gonic/gin" "github.com/samber/lo" "github.com/stretchr/testify/require" diff --git a/relay/helper/common.go b/relay/helper/common.go index 5b118aef..fa363569 100644 --- a/relay/helper/common.go +++ b/relay/helper/common.go @@ -6,9 +6,9 @@ import ( "net/http" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" diff --git a/relay/helper/model_mapped.go b/relay/helper/model_mapped.go index 5d6efa09..cf74dbd2 100644 --- a/relay/helper/model_mapped.go +++ b/relay/helper/model_mapped.go @@ -6,9 +6,9 @@ import ( "fmt" "strings" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting/ratio_setting" "github.com/gin-gonic/gin" ) diff --git a/relay/helper/openai_image_request_test.go b/relay/helper/openai_image_request_test.go index e9fb1b99..45557113 100644 --- a/relay/helper/openai_image_request_test.go +++ b/relay/helper/openai_image_request_test.go @@ -11,8 +11,8 @@ import ( "testing" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) @@ -51,7 +51,7 @@ func TestGetAndValidOpenAIImageRequestMultipartStream(t *testing.T) { require.NoError(t, err) require.NotNil(t, req.Stream) require.True(t, *req.Stream) - require.True(t, req.IsStream(c)) + require.True(t, req.IsStream(c.Request)) bodyAfterValidation, err := io.ReadAll(c.Request.Body) require.NoError(t, err) diff --git a/relay/helper/price.go b/relay/helper/price.go index 2e8ebb2d..b85e29b6 100644 --- a/relay/helper/price.go +++ b/relay/helper/price.go @@ -9,10 +9,10 @@ import ( "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/pkg/billingexpr" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/billing_setting" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/QuantumNous/new-api/setting/ratio_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/helper/price_test.go b/relay/helper/price_test.go index ef396146..0f28b5a4 100644 --- a/relay/helper/price_test.go +++ b/relay/helper/price_test.go @@ -8,10 +8,10 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/pkg/billingexpr" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/billing_setting" "github.com/QuantumNous/new-api/setting/config" "github.com/QuantumNous/new-api/setting/ratio_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) diff --git a/relay/helper/valid_request.go b/relay/helper/valid_request.go index 22068a45..47d39c2e 100644 --- a/relay/helper/valid_request.go +++ b/relay/helper/valid_request.go @@ -9,10 +9,10 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relayconstant "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/samber/lo" "github.com/gin-gonic/gin" diff --git a/relay/image_handler.go b/relay/image_handler.go index 0c21fabc..f970a43e 100644 --- a/relay/image_handler.go +++ b/relay/image_handler.go @@ -9,13 +9,13 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/param_override_error.go b/relay/param_override_error.go index c2338298..a590f55a 100644 --- a/relay/param_override_error.go +++ b/relay/param_override_error.go @@ -2,7 +2,7 @@ package relay import ( relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" ) func newAPIErrorFromParamOverride(err error) *types.NewAPIError { diff --git a/relay/rerank_handler.go b/relay/rerank_handler.go index f1c19e27..769feb22 100644 --- a/relay/rerank_handler.go +++ b/relay/rerank_handler.go @@ -6,13 +6,13 @@ import ( "net/http" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/responses_handler.go b/relay/responses_handler.go index 381c41e2..c736c6a5 100644 --- a/relay/responses_handler.go +++ b/relay/responses_handler.go @@ -7,14 +7,14 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/websocket.go b/relay/websocket.go index 57a51895..bf59663d 100644 --- a/relay/websocket.go +++ b/relay/websocket.go @@ -3,10 +3,10 @@ package relay import ( "fmt" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" diff --git a/dto/alpha_search_request.go b/relaykit/dto/alpha_search_request.go similarity index 85% rename from dto/alpha_search_request.go rename to relaykit/dto/alpha_search_request.go index 7628a6a6..e0d6a6f3 100644 --- a/dto/alpha_search_request.go +++ b/relaykit/dto/alpha_search_request.go @@ -2,10 +2,9 @@ package dto import ( "encoding/json" + "net/http" - "github.com/QuantumNous/new-api/types" - - "github.com/gin-gonic/gin" + "github.com/QuantumNous/new-api/relaykit/types" ) // AlphaSearchRequest is the Codex standalone web search request. @@ -28,7 +27,7 @@ func (r *AlphaSearchRequest) GetTokenCountMeta() *types.TokenCountMeta { } } -func (r *AlphaSearchRequest) IsStream(c *gin.Context) bool { +func (r *AlphaSearchRequest) IsStream(_ *http.Request) bool { return false } diff --git a/dto/audio.go b/relaykit/dto/audio.go similarity index 95% rename from dto/audio.go rename to relaykit/dto/audio.go index e0d4f9d0..5f86e2ed 100644 --- a/dto/audio.go +++ b/relaykit/dto/audio.go @@ -2,11 +2,10 @@ package dto import ( "encoding/json" + "net/http" "strings" - "github.com/QuantumNous/new-api/types" - - "github.com/gin-gonic/gin" + "github.com/QuantumNous/new-api/relaykit/types" ) type AudioRequest struct { @@ -41,7 +40,7 @@ func (r *AudioRequest) GetTokenCountMeta() *types.TokenCountMeta { return meta } -func (r *AudioRequest) IsStream(c *gin.Context) bool { +func (r *AudioRequest) IsStream(c *http.Request) bool { return r.StreamFormat == "sse" } diff --git a/dto/billing_usage.go b/relaykit/dto/billing_usage.go similarity index 100% rename from dto/billing_usage.go rename to relaykit/dto/billing_usage.go diff --git a/dto/billing_usage_test.go b/relaykit/dto/billing_usage_test.go similarity index 96% rename from dto/billing_usage_test.go rename to relaykit/dto/billing_usage_test.go index bc5e969b..a09e2ac5 100644 --- a/dto/billing_usage_test.go +++ b/relaykit/dto/billing_usage_test.go @@ -3,7 +3,7 @@ package dto import ( "testing" - "github.com/QuantumNous/new-api/common" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -70,7 +70,7 @@ func TestBillingUsageJSONUsesProtocolNamedFields(t *testing.T) { GeminiUsageMetadata: &GeminiUsageMetadata{PromptTokenCount: 3, BillingUsage: NewOpenAIChatBillingUsage(&Usage{PromptTokens: 7})}, } - data, err := common.Marshal(billingUsage) + data, err := kitutil.Marshal(billingUsage) require.NoError(t, err) assert.Contains(t, string(data), `"openai_usage"`) diff --git a/dto/channel_settings.go b/relaykit/dto/channel_settings.go similarity index 96% rename from dto/channel_settings.go rename to relaykit/dto/channel_settings.go index b938521c..3d7937d3 100644 --- a/dto/channel_settings.go +++ b/relaykit/dto/channel_settings.go @@ -7,7 +7,7 @@ import ( "strings" "sync" - "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/relaykit/types" ) type ChannelSettings struct { @@ -173,13 +173,13 @@ func (c *AdvancedCustomConfig) SupportsPathForModel(requestPath string, model st return ok } -func (c *AdvancedCustomConfig) SupportedEndpointTypesForModel(model string) []constant.EndpointType { +func (c *AdvancedCustomConfig) SupportedEndpointTypesForModel(model string) []types.EndpointType { if c == nil { return nil } model = strings.TrimSpace(model) - endpoints := make([]constant.EndpointType, 0, len(c.Routes)) - seen := make(map[constant.EndpointType]struct{}, len(c.Routes)) + endpoints := make([]types.EndpointType, 0, len(c.Routes)) + seen := make(map[types.EndpointType]struct{}, len(c.Routes)) for _, route := range c.Routes { if !matchAdvancedCustomRouteModel(route.Models, model) { continue @@ -197,27 +197,27 @@ func (c *AdvancedCustomConfig) SupportedEndpointTypesForModel(model string) []co return endpoints } -func advancedCustomEndpointTypeFromIncomingPath(incomingPath string) (constant.EndpointType, bool) { +func advancedCustomEndpointTypeFromIncomingPath(incomingPath string) (types.EndpointType, bool) { switch incomingPath { case advancedCustomEndpointPathOpenAIChat: - return constant.EndpointTypeOpenAI, true + return types.EndpointTypeOpenAI, true case advancedCustomEndpointPathOpenAIResponses: - return constant.EndpointTypeOpenAIResponse, true + return types.EndpointTypeOpenAIResponse, true case advancedCustomEndpointPathOpenAIResponsesCompact: - return constant.EndpointTypeOpenAIResponseCompact, true + return types.EndpointTypeOpenAIResponseCompact, true case advancedCustomEndpointPathOpenAIAlphaSearch: - return constant.EndpointTypeOpenAIAlphaSearch, true + return types.EndpointTypeOpenAIAlphaSearch, true case advancedCustomEndpointPathClaudeMessages: - return constant.EndpointTypeAnthropic, true + return types.EndpointTypeAnthropic, true case advancedCustomEndpointPathJinaRerank: - return constant.EndpointTypeJinaRerank, true + return types.EndpointTypeJinaRerank, true case advancedCustomEndpointPathImageGeneration: - return constant.EndpointTypeImageGeneration, true + return types.EndpointTypeImageGeneration, true case advancedCustomEndpointPathEmbeddings: - return constant.EndpointTypeEmbeddings, true + return types.EndpointTypeEmbeddings, true default: if isAdvancedCustomGeminiIncomingPath(incomingPath) { - return constant.EndpointTypeGemini, true + return types.EndpointTypeGemini, true } return "", false } diff --git a/dto/channel_settings_test.go b/relaykit/dto/channel_settings_test.go similarity index 97% rename from dto/channel_settings_test.go rename to relaykit/dto/channel_settings_test.go index 9b42516e..90cc6fff 100644 --- a/dto/channel_settings_test.go +++ b/relaykit/dto/channel_settings_test.go @@ -4,7 +4,7 @@ import ( "regexp" "testing" - "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -464,17 +464,17 @@ func TestAdvancedCustomSupportedEndpointTypesForModel(t *testing.T) { } require.NoError(t, config.Validate()) - assert.Equal(t, []constant.EndpointType{ - constant.EndpointTypeOpenAIResponse, - constant.EndpointTypeGemini, - constant.EndpointTypeAnthropic, + assert.Equal(t, []types.EndpointType{ + types.EndpointTypeOpenAIResponse, + types.EndpointTypeGemini, + types.EndpointTypeAnthropic, }, config.SupportedEndpointTypesForModel("gemini-2.5-flash")) - assert.Equal(t, []constant.EndpointType{ - constant.EndpointTypeOpenAI, - constant.EndpointTypeAnthropic, + assert.Equal(t, []types.EndpointType{ + types.EndpointTypeOpenAI, + types.EndpointTypeAnthropic, }, config.SupportedEndpointTypesForModel("gpt-4o")) - assert.Equal(t, []constant.EndpointType{ - constant.EndpointTypeAnthropic, + assert.Equal(t, []types.EndpointType{ + types.EndpointTypeAnthropic, }, config.SupportedEndpointTypesForModel("other-model")) } @@ -489,8 +489,8 @@ func TestAdvancedCustomValidateAlphaSearchConverterPath(t *testing.T) { }, } require.NoError(t, valid.Validate()) - assert.Equal(t, []constant.EndpointType{ - constant.EndpointTypeOpenAIAlphaSearch, + assert.Equal(t, []types.EndpointType{ + types.EndpointTypeOpenAIAlphaSearch, }, valid.SupportedEndpointTypesForModel("gpt-5.1")) nonNoneConverters := []string{ diff --git a/dto/claude.go b/relaykit/dto/claude.go similarity index 96% rename from dto/claude.go rename to relaykit/dto/claude.go index 0c552b4b..a3a41e75 100644 --- a/dto/claude.go +++ b/relaykit/dto/claude.go @@ -3,12 +3,11 @@ package dto import ( "encoding/json" "fmt" + "net/http" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/types" - - "github.com/gin-gonic/gin" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" + "github.com/QuantumNous/new-api/relaykit/types" ) type ClaudeMetadata struct { @@ -85,7 +84,7 @@ func (c *ClaudeMediaMessage) GetStringContent() string { } func (c *ClaudeMediaMessage) GetJsonRowString() string { - jsonContent, _ := common.Marshal(c) + jsonContent, _ := kitutil.Marshal(c) return string(jsonContent) } @@ -94,7 +93,7 @@ func (c *ClaudeMediaMessage) SetContent(content any) { } func (c *ClaudeMediaMessage) ParseMediaContent() []ClaudeMediaMessage { - mediaContent, _ := common.Any2Type[[]ClaudeMediaMessage](c.Content) + mediaContent, _ := kitutil.Any2Type[[]ClaudeMediaMessage](c.Content) return mediaContent } @@ -104,7 +103,7 @@ func (m *ClaudeMediaMessage) ToFileSource() types.FileSource { } data := m.Source.Url if data == "" { - data = common.Interface2String(m.Source.Data) + data = kitutil.Interface2String(m.Source.Data) } if data == "" { return nil @@ -167,7 +166,7 @@ func (c *ClaudeMessage) SetContent(content any) { } func (c *ClaudeMessage) ParseContent() ([]ClaudeMediaMessage, error) { - return common.Any2Type[[]ClaudeMediaMessage](c.Content) + return kitutil.Any2Type[[]ClaudeMediaMessage](c.Content) } type Tool struct { @@ -308,12 +307,12 @@ func (c *ClaudeRequest) GetTokenCountMeta() *types.TokenCountMeta { texts = append(texts, media.Name) } if media.Input != nil { - b, _ := common.Marshal(media.Input) + b, _ := kitutil.Marshal(media.Input) texts = append(texts, string(b)) } case "tool_result": if media.Content != nil { - b, _ := common.Marshal(media.Content) + b, _ := kitutil.Marshal(media.Content) texts = append(texts, string(b)) } } @@ -334,7 +333,7 @@ func (c *ClaudeRequest) GetTokenCountMeta() *types.TokenCountMeta { texts = append(texts, t.Description) } if t.InputSchema != nil { - b, _ := common.Marshal(t.InputSchema) + b, _ := kitutil.Marshal(t.InputSchema) texts = append(texts, string(b)) } } @@ -346,7 +345,7 @@ func (c *ClaudeRequest) GetTokenCountMeta() *types.TokenCountMeta { texts = append(texts, t.Name) } if t.UserLocation != nil { - b, _ := common.Marshal(t.UserLocation) + b, _ := kitutil.Marshal(t.UserLocation) texts = append(texts, string(b)) } } @@ -358,7 +357,7 @@ func (c *ClaudeRequest) GetTokenCountMeta() *types.TokenCountMeta { return &tokenCountMeta } -func (c *ClaudeRequest) IsStream(ctx *gin.Context) bool { +func (c *ClaudeRequest) IsStream(ctx *http.Request) bool { if c.Stream == nil { return false } @@ -479,7 +478,7 @@ func (c *ClaudeRequest) SetStringSystem(system string) { } func (c *ClaudeRequest) ParseSystem() []ClaudeMediaMessage { - mediaContent, _ := common.Any2Type[[]ClaudeMediaMessage](c.System) + mediaContent, _ := kitutil.Any2Type[[]ClaudeMediaMessage](c.System) return mediaContent } diff --git a/dto/embedding.go b/relaykit/dto/embedding.go similarity index 94% rename from dto/embedding.go rename to relaykit/dto/embedding.go index c9bd2d70..65a6b54b 100644 --- a/dto/embedding.go +++ b/relaykit/dto/embedding.go @@ -1,11 +1,10 @@ package dto import ( + "net/http" "strings" - "github.com/QuantumNous/new-api/types" - - "github.com/gin-gonic/gin" + "github.com/QuantumNous/new-api/relaykit/types" ) type EmbeddingOptions struct { @@ -45,7 +44,7 @@ func (r *EmbeddingRequest) GetTokenCountMeta() *types.TokenCountMeta { } } -func (r *EmbeddingRequest) IsStream(c *gin.Context) bool { +func (r *EmbeddingRequest) IsStream(c *http.Request) bool { return false } diff --git a/dto/error.go b/relaykit/dto/error.go similarity index 85% rename from dto/error.go rename to relaykit/dto/error.go index be57407f..0f83d2b6 100644 --- a/dto/error.go +++ b/relaykit/dto/error.go @@ -3,8 +3,8 @@ package dto import ( "encoding/json" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/types" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" + "github.com/QuantumNous/new-api/relaykit/types" ) //type OpenAIError struct { @@ -41,7 +41,7 @@ type GeneralErrorResponse struct { func (e GeneralErrorResponse) TryToOpenAIError() *types.OpenAIError { var openAIError types.OpenAIError if len(e.Error) > 0 { - err := common.Unmarshal(e.Error, &openAIError) + err := kitutil.Unmarshal(e.Error, &openAIError) if err == nil && openAIError.Message != "" { return &openAIError } @@ -51,16 +51,16 @@ func (e GeneralErrorResponse) TryToOpenAIError() *types.OpenAIError { func (e GeneralErrorResponse) ToMessage() string { if len(e.Error) > 0 { - switch common.GetJsonType(e.Error) { + switch kitutil.GetJsonType(e.Error) { case "object": var openAIError types.OpenAIError - err := common.Unmarshal(e.Error, &openAIError) + err := kitutil.Unmarshal(e.Error, &openAIError) if err == nil && openAIError.Message != "" { return openAIError.Message } case "string": var msg string - err := common.Unmarshal(e.Error, &msg) + err := kitutil.Unmarshal(e.Error, &msg) if err == nil && msg != "" { return msg } diff --git a/dto/gemini.go b/relaykit/dto/gemini.go similarity index 94% rename from dto/gemini.go rename to relaykit/dto/gemini.go index ab285dfa..033fcc0f 100644 --- a/dto/gemini.go +++ b/relaykit/dto/gemini.go @@ -2,13 +2,11 @@ package dto import ( "encoding/json" + "net/http" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/logger" - "github.com/QuantumNous/new-api/types" - - "github.com/gin-gonic/gin" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" + "github.com/QuantumNous/new-api/relaykit/types" ) type GeminiChatRequest struct { @@ -30,7 +28,7 @@ func (r *GeminiChatRequest) UnmarshalJSON(data []byte) error { SystemInstructionSnake *GeminiChatContent `json:"system_instruction,omitempty"` } - if err := common.Unmarshal(data, &aux); err != nil { + if err := kitutil.Unmarshal(data, &aux); err != nil { return err } @@ -108,13 +106,16 @@ func (r *GeminiChatRequest) GetTokenCountMeta() *types.TokenCountMeta { } } -func (r *GeminiChatRequest) IsStream(c *gin.Context) bool { - if c.Query("alt") == "sse" { +func (r *GeminiChatRequest) IsStream(c *http.Request) bool { + if c == nil { + return false + } + if c.URL.Query().Get("alt") == "sse" { return true } // Native Gemini API uses URL action to indicate streaming: // /v1beta/models/{model}:streamGenerateContent - if strings.Contains(c.Request.URL.Path, "streamGenerateContent") { + if strings.Contains(c.URL.Path, "streamGenerateContent") { return true } return false @@ -128,15 +129,15 @@ func (r *GeminiChatRequest) GetTools() []GeminiChatTool { var tools []GeminiChatTool if strings.HasPrefix(string(r.Tools), "[") { // is array - if err := common.Unmarshal(r.Tools, &tools); err != nil { - logger.LogError(nil, "error_unmarshalling_tools: "+err.Error()) + if err := kitutil.Unmarshal(r.Tools, &tools); err != nil { + kitutil.LogError("error_unmarshalling_tools: " + err.Error()) return nil } } else if strings.HasPrefix(string(r.Tools), "{") { // is object singleTool := GeminiChatTool{} - if err := common.Unmarshal(r.Tools, &singleTool); err != nil { - logger.LogError(nil, "error_unmarshalling_single_tool: "+err.Error()) + if err := kitutil.Unmarshal(r.Tools, &singleTool); err != nil { + kitutil.LogError("error_unmarshalling_single_tool: " + err.Error()) return nil } tools = []GeminiChatTool{singleTool} @@ -151,9 +152,9 @@ func (r *GeminiChatRequest) SetTools(tools []GeminiChatTool) { } // Marshal the tools to JSON - data, err := common.Marshal(tools) + data, err := kitutil.Marshal(tools) if err != nil { - logger.LogError(nil, "error_marshalling_tools: "+err.Error()) + kitutil.LogError("error_marshalling_tools: " + err.Error()) return } r.Tools = data @@ -176,7 +177,7 @@ func (c *GeminiThinkingConfig) UnmarshalJSON(data []byte) error { ThinkingLevelSnake string `json:"thinking_level,omitempty"` } - if err := common.Unmarshal(data, &aux); err != nil { + if err := kitutil.Unmarshal(data, &aux); err != nil { return err } @@ -221,7 +222,7 @@ func (g *GeminiInlineData) UnmarshalJSON(data []byte) error { MimeTypeSnake string `json:"mime_type"` } - if err := common.Unmarshal(data, &aux); err != nil { + if err := kitutil.Unmarshal(data, &aux); err != nil { return err } @@ -290,7 +291,7 @@ func (p *GeminiPart) UnmarshalJSON(data []byte) error { InlineDataSnake *GeminiInlineData `json:"inline_data,omitempty"` // snake_case variant } - if err := common.Unmarshal(data, &aux); err != nil { + if err := kitutil.Unmarshal(data, &aux); err != nil { return err } @@ -373,7 +374,7 @@ func (c *GeminiChatGenerationConfig) UnmarshalJSON(data []byte) error { ImageConfigSnake json.RawMessage `json:"image_config,omitempty"` } - if err := common.Unmarshal(data, &aux); err != nil { + if err := kitutil.Unmarshal(data, &aux); err != nil { return err } @@ -478,7 +479,7 @@ func (r *GeminiChatResponse) UnmarshalJSON(data []byte) error { PromptFeedback *GeminiChatPromptFeedback `json:"promptFeedback,omitempty"` UsageMetadata *GeminiUsageMetadata `json:"usageMetadata"` } - if err := common.Unmarshal(data, &aux); err != nil { + if err := kitutil.Unmarshal(data, &aux); err != nil { return err } r.Candidates = aux.Candidates @@ -557,7 +558,7 @@ type GeminiEmbeddingRequest struct { OutputDimensionality int `json:"outputDimensionality,omitempty"` } -func (r *GeminiEmbeddingRequest) IsStream(c *gin.Context) bool { +func (r *GeminiEmbeddingRequest) IsStream(c *http.Request) bool { // Gemini embedding requests are not streamed return false } @@ -585,7 +586,7 @@ type GeminiBatchEmbeddingRequest struct { Requests []*GeminiEmbeddingRequest `json:"requests"` } -func (r *GeminiBatchEmbeddingRequest) IsStream(c *gin.Context) bool { +func (r *GeminiBatchEmbeddingRequest) IsStream(c *http.Request) bool { // Gemini batch embedding requests are not streamed return false } diff --git a/dto/gemini_generation_config_test.go b/relaykit/dto/gemini_generation_config_test.go similarity index 87% rename from dto/gemini_generation_config_test.go rename to relaykit/dto/gemini_generation_config_test.go index ed4beb30..0eca0bc7 100644 --- a/dto/gemini_generation_config_test.go +++ b/relaykit/dto/gemini_generation_config_test.go @@ -3,7 +3,7 @@ package dto import ( "testing" - "github.com/QuantumNous/new-api/common" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -22,13 +22,13 @@ func TestGeminiChatGenerationConfigPreservesExplicitZeroValuesCamelCase(t *testi }`) var req GeminiChatRequest - require.NoError(t, common.Unmarshal(raw, &req)) + require.NoError(t, kitutil.Unmarshal(raw, &req)) - encoded, err := common.Marshal(req) + encoded, err := kitutil.Marshal(req) require.NoError(t, err) var out map[string]any - require.NoError(t, common.Unmarshal(encoded, &out)) + require.NoError(t, kitutil.Unmarshal(encoded, &out)) generationConfig, ok := out["generationConfig"].(map[string]any) require.True(t, ok) @@ -62,13 +62,13 @@ func TestGeminiChatGenerationConfigPreservesExplicitZeroValuesSnakeCase(t *testi }`) var req GeminiChatRequest - require.NoError(t, common.Unmarshal(raw, &req)) + require.NoError(t, kitutil.Unmarshal(raw, &req)) - encoded, err := common.Marshal(req) + encoded, err := kitutil.Marshal(req) require.NoError(t, err) var out map[string]any - require.NoError(t, common.Unmarshal(encoded, &out)) + require.NoError(t, kitutil.Unmarshal(encoded, &out)) generationConfig, ok := out["generationConfig"].(map[string]any) require.True(t, ok) diff --git a/dto/gemini_isstream_test.go b/relaykit/dto/gemini_isstream_test.go similarity index 85% rename from dto/gemini_isstream_test.go rename to relaykit/dto/gemini_isstream_test.go index ecd461b9..f05a24db 100644 --- a/dto/gemini_isstream_test.go +++ b/relaykit/dto/gemini_isstream_test.go @@ -2,16 +2,12 @@ package dto import ( "net/http" - "net/http/httptest" "testing" - "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) func TestGeminiChatRequest_IsStream(t *testing.T) { - gin.SetMode(gin.TestMode) - tests := []struct { name string path string @@ -58,16 +54,15 @@ func TestGeminiChatRequest_IsStream(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) url := tt.path if tt.query != "" { url += "?" + tt.query } - c.Request, _ = http.NewRequest("POST", url, nil) + httpReq, err := http.NewRequest("POST", url, nil) + assert.NoError(t, err) req := &GeminiChatRequest{} - assert.Equal(t, tt.expected, req.IsStream(c)) + assert.Equal(t, tt.expected, req.IsStream(httpReq)) }) } } diff --git a/dto/gemini_response_test.go b/relaykit/dto/gemini_response_test.go similarity index 67% rename from dto/gemini_response_test.go rename to relaykit/dto/gemini_response_test.go index c12994bb..3ec1ff33 100644 --- a/dto/gemini_response_test.go +++ b/relaykit/dto/gemini_response_test.go @@ -3,32 +3,32 @@ package dto import ( "testing" - "github.com/QuantumNous/new-api/common" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGeminiChatResponseUsageMetadataPresence(t *testing.T) { var missing GeminiChatResponse - require.NoError(t, common.Unmarshal([]byte(`{"candidates":[]}`), &missing)) + require.NoError(t, kitutil.Unmarshal([]byte(`{"candidates":[]}`), &missing)) assert.False(t, missing.HasUsageMetadata) assert.Nil(t, missing.GetUsageMetadata()) var empty GeminiChatResponse - require.NoError(t, common.Unmarshal([]byte(`{"candidates":[],"usageMetadata":{}}`), &empty)) + require.NoError(t, kitutil.Unmarshal([]byte(`{"candidates":[],"usageMetadata":{}}`), &empty)) assert.True(t, empty.HasUsageMetadata) require.NotNil(t, empty.GetUsageMetadata()) assert.False(t, HasGeminiUsageMetadataTokens(empty.GetUsageMetadata())) var populated GeminiChatResponse - require.NoError(t, common.Unmarshal([]byte(`{"candidates":[],"usageMetadata":{"promptTokenCount":3}}`), &populated)) + require.NoError(t, kitutil.Unmarshal([]byte(`{"candidates":[],"usageMetadata":{"promptTokenCount":3}}`), &populated)) assert.True(t, populated.HasUsageMetadata) require.NotNil(t, populated.GetUsageMetadata()) assert.True(t, HasGeminiUsageMetadataTokens(populated.GetUsageMetadata())) } func TestGeminiChatResponseMarshalKeepsUsageMetadataField(t *testing.T) { - data, err := common.Marshal(GeminiChatResponse{}) + data, err := kitutil.Marshal(GeminiChatResponse{}) require.NoError(t, err) assert.Contains(t, string(data), `"usageMetadata"`) } diff --git a/dto/notify.go b/relaykit/dto/notify.go similarity index 100% rename from dto/notify.go rename to relaykit/dto/notify.go diff --git a/dto/openai_compaction.go b/relaykit/dto/openai_compaction.go similarity index 90% rename from dto/openai_compaction.go rename to relaykit/dto/openai_compaction.go index f19df09c..3aed832c 100644 --- a/dto/openai_compaction.go +++ b/relaykit/dto/openai_compaction.go @@ -3,7 +3,7 @@ package dto import ( "encoding/json" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" ) type OpenAIResponsesCompactionResponse struct { diff --git a/dto/openai_image.go b/relaykit/dto/openai_image.go similarity index 91% rename from dto/openai_image.go rename to relaykit/dto/openai_image.go index 275fd555..ef9c6fb0 100644 --- a/dto/openai_image.go +++ b/relaykit/dto/openai_image.go @@ -2,13 +2,12 @@ package dto import ( "encoding/json" + "net/http" "reflect" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/types" - - "github.com/gin-gonic/gin" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" + "github.com/QuantumNous/new-api/relaykit/types" ) // MaxImageN caps the image generation count. Without this bound a huge or @@ -46,7 +45,7 @@ type ImageRequest struct { func (i *ImageRequest) UnmarshalJSON(data []byte) error { // 先解析成 map[string]interface{} var rawMap map[string]json.RawMessage - if err := common.Unmarshal(data, &rawMap); err != nil { + if err := kitutil.Unmarshal(data, &rawMap); err != nil { return err } @@ -56,7 +55,7 @@ func (i *ImageRequest) UnmarshalJSON(data []byte) error { // 再正常解析已定义字段 type Alias ImageRequest var known Alias - if err := common.Unmarshal(data, &known); err != nil { + if err := kitutil.Unmarshal(data, &known); err != nil { return err } *i = ImageRequest(known) @@ -76,13 +75,13 @@ func (r ImageRequest) MarshalJSON() ([]byte, error) { // 将已定义字段转为 map type Alias ImageRequest alias := Alias(r) - base, err := common.Marshal(alias) + base, err := kitutil.Marshal(alias) if err != nil { return nil, err } var baseMap map[string]json.RawMessage - if err := common.Unmarshal(base, &baseMap); err != nil { + if err := kitutil.Unmarshal(base, &baseMap); err != nil { return nil, err } @@ -94,7 +93,7 @@ func (r ImageRequest) MarshalJSON() ([]byte, error) { // } //} - return common.Marshal(baseMap) + return kitutil.Marshal(baseMap) } func GetJSONFieldNames(t reflect.Type) map[string]struct{} { @@ -171,7 +170,7 @@ func (i *ImageRequest) GetTokenCountMeta() *types.TokenCountMeta { } } -func (i *ImageRequest) IsStream(c *gin.Context) bool { +func (i *ImageRequest) IsStream(c *http.Request) bool { return i.Stream != nil && *i.Stream } diff --git a/dto/openai_request.go b/relaykit/dto/openai_request.go similarity index 95% rename from dto/openai_request.go rename to relaykit/dto/openai_request.go index 3bb2b34c..e646658f 100644 --- a/dto/openai_request.go +++ b/relaykit/dto/openai_request.go @@ -3,13 +3,12 @@ package dto import ( "encoding/json" "fmt" + "net/http" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/types" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/samber/lo" - - "github.com/gin-gonic/gin" ) type ResponseFormat struct { @@ -196,7 +195,7 @@ func (r *GeneralOpenAIRequest) GetTokenCountMeta() *types.TokenCountMeta { return &tokenCountMeta } -func (r *GeneralOpenAIRequest) IsStream(c *gin.Context) bool { +func (r *GeneralOpenAIRequest) IsStream(c *http.Request) bool { return lo.FromPtrOr(r.Stream, false) } @@ -208,8 +207,8 @@ func (r *GeneralOpenAIRequest) SetModelName(modelName string) { func (r *GeneralOpenAIRequest) ToMap() map[string]any { result := make(map[string]any) - data, _ := common.Marshal(r) - _ = common.Unmarshal(data, &result) + data, _ := kitutil.Marshal(r) + _ = kitutil.Unmarshal(data, &result) return result } @@ -315,9 +314,9 @@ func (m *MediaContent) GetImageMedia() *MessageImageUrl { } if itemMap, ok := m.ImageUrl.(map[string]any); ok { out := &MessageImageUrl{ - Url: common.Interface2String(itemMap["url"]), - Detail: common.Interface2String(itemMap["detail"]), - MimeType: common.Interface2String(itemMap["mime_type"]), + Url: kitutil.Interface2String(itemMap["url"]), + Detail: kitutil.Interface2String(itemMap["detail"]), + MimeType: kitutil.Interface2String(itemMap["mime_type"]), } return out } @@ -332,8 +331,8 @@ func (m *MediaContent) GetInputAudio() *MessageInputAudio { } if itemMap, ok := m.InputAudio.(map[string]any); ok { out := &MessageInputAudio{ - Data: common.Interface2String(itemMap["data"]), - Format: common.Interface2String(itemMap["format"]), + Data: kitutil.Interface2String(itemMap["data"]), + Format: kitutil.Interface2String(itemMap["format"]), } return out } @@ -348,9 +347,9 @@ func (m *MediaContent) GetFile() *MessageFile { } if itemMap, ok := m.File.(map[string]any); ok { out := &MessageFile{ - FileName: common.Interface2String(itemMap["file_name"]), - FileData: common.Interface2String(itemMap["file_data"]), - FileId: common.Interface2String(itemMap["file_id"]), + FileName: kitutil.Interface2String(itemMap["file_name"]), + FileData: kitutil.Interface2String(itemMap["file_data"]), + FileId: kitutil.Interface2String(itemMap["file_id"]), } return out } @@ -365,7 +364,7 @@ func (m *MediaContent) GetVideoUrl() *MessageVideoUrl { } if itemMap, ok := m.VideoUrl.(map[string]any); ok { out := &MessageVideoUrl{ - Url: common.Interface2String(itemMap["url"]), + Url: kitutil.Interface2String(itemMap["url"]), } return out } @@ -944,7 +943,7 @@ func (r *OpenAIResponsesRequest) GetTokenCountMeta() *types.TokenCountMeta { } } -func (r *OpenAIResponsesRequest) IsStream(c *gin.Context) bool { +func (r *OpenAIResponsesRequest) IsStream(c *http.Request) bool { return lo.FromPtrOr(r.Stream, false) } @@ -957,7 +956,7 @@ func (r *OpenAIResponsesRequest) SetModelName(modelName string) { func (r *OpenAIResponsesRequest) GetToolsMap() []map[string]any { var toolsMap []map[string]any if len(r.Tools) > 0 { - _ = common.Unmarshal(r.Tools, &toolsMap) + _ = kitutil.Unmarshal(r.Tools, &toolsMap) } return toolsMap } @@ -996,31 +995,31 @@ func (r *OpenAIResponsesRequest) ParseInput() []MediaInput { var mediaInputs []MediaInput // Try string first - // if str, ok := common.GetJsonType(r.Input); ok { + // if str, ok := kitutil.GetJsonType(r.Input); ok { // inputs = append(inputs, MediaInput{Type: "input_text", Text: str}) // return inputs // } - if common.GetJsonType(r.Input) == "string" { + if kitutil.GetJsonType(r.Input) == "string" { var str string - _ = common.Unmarshal(r.Input, &str) + _ = kitutil.Unmarshal(r.Input, &str) mediaInputs = append(mediaInputs, MediaInput{Type: "input_text", Text: str}) return mediaInputs } // Try array of parts - if common.GetJsonType(r.Input) == "array" { + if kitutil.GetJsonType(r.Input) == "array" { var inputs []Input - _ = common.Unmarshal(r.Input, &inputs) + _ = kitutil.Unmarshal(r.Input, &inputs) for _, input := range inputs { - if common.GetJsonType(input.Content) == "string" { + if kitutil.GetJsonType(input.Content) == "string" { var str string - _ = common.Unmarshal(input.Content, &str) + _ = kitutil.Unmarshal(input.Content, &str) mediaInputs = append(mediaInputs, MediaInput{Type: "input_text", Text: str}) } - if common.GetJsonType(input.Content) == "array" { + if kitutil.GetJsonType(input.Content) == "array" { var array []any - _ = common.Unmarshal(input.Content, &array) + _ = kitutil.Unmarshal(input.Content, &array) for _, itemAny := range array { // Already parsed MediaContent if media, ok := itemAny.(MediaInput); ok { diff --git a/dto/openai_request_zero_value_test.go b/relaykit/dto/openai_request_zero_value_test.go similarity index 92% rename from dto/openai_request_zero_value_test.go rename to relaykit/dto/openai_request_zero_value_test.go index e9d9e30e..cd3c1bea 100644 --- a/dto/openai_request_zero_value_test.go +++ b/relaykit/dto/openai_request_zero_value_test.go @@ -3,7 +3,7 @@ package dto import ( "testing" - "github.com/QuantumNous/new-api/common" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" ) @@ -28,10 +28,10 @@ func TestGeneralOpenAIRequestPreserveExplicitZeroValues(t *testing.T) { }`) var req GeneralOpenAIRequest - err := common.Unmarshal(raw, &req) + err := kitutil.Unmarshal(raw, &req) require.NoError(t, err) - encoded, err := common.Marshal(req) + encoded, err := kitutil.Marshal(req) require.NoError(t, err) require.True(t, gjson.GetBytes(encoded, "stream").Exists()) @@ -60,10 +60,10 @@ func TestOpenAIResponsesRequestPreserveExplicitZeroValues(t *testing.T) { }`) var req OpenAIResponsesRequest - err := common.Unmarshal(raw, &req) + err := kitutil.Unmarshal(raw, &req) require.NoError(t, err) - encoded, err := common.Marshal(req) + encoded, err := kitutil.Marshal(req) require.NoError(t, err) require.True(t, gjson.GetBytes(encoded, "max_output_tokens").Exists()) diff --git a/dto/openai_response.go b/relaykit/dto/openai_response.go similarity index 98% rename from dto/openai_response.go rename to relaykit/dto/openai_response.go index 7959f074..945d0a86 100644 --- a/dto/openai_response.go +++ b/relaykit/dto/openai_response.go @@ -4,8 +4,8 @@ import ( "encoding/json" "fmt" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/types" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" + "github.com/QuantumNous/new-api/relaykit/types" ) const ( @@ -348,7 +348,7 @@ func (r *ResponsesOutput) ArgumentsString() string { // ResponsesArgumentsString returns function call arguments in the string form expected by Chat Completions. func ResponsesArgumentsString(arguments json.RawMessage) string { - return common.JsonRawMessageToString(arguments) + return kitutil.JsonRawMessageToString(arguments) } type ResponsesOutputContent struct { diff --git a/dto/openai_responses_compaction_request.go b/relaykit/dto/openai_responses_compaction_request.go similarity index 91% rename from dto/openai_responses_compaction_request.go rename to relaykit/dto/openai_responses_compaction_request.go index f3d1cb66..bc11879a 100644 --- a/dto/openai_responses_compaction_request.go +++ b/relaykit/dto/openai_responses_compaction_request.go @@ -2,11 +2,10 @@ package dto import ( "encoding/json" + "net/http" "strings" - "github.com/QuantumNous/new-api/types" - - "github.com/gin-gonic/gin" + "github.com/QuantumNous/new-api/relaykit/types" ) type OpenAIResponsesCompactionRequest struct { @@ -40,7 +39,7 @@ func (r *OpenAIResponsesCompactionRequest) GetTokenCountMeta() *types.TokenCount } } -func (r *OpenAIResponsesCompactionRequest) IsStream(c *gin.Context) bool { +func (r *OpenAIResponsesCompactionRequest) IsStream(c *http.Request) bool { return false } diff --git a/dto/openai_video.go b/relaykit/dto/openai_video.go similarity index 100% rename from dto/openai_video.go rename to relaykit/dto/openai_video.go diff --git a/dto/playground.go b/relaykit/dto/playground.go similarity index 100% rename from dto/playground.go rename to relaykit/dto/playground.go diff --git a/dto/pricing.go b/relaykit/dto/pricing.go similarity index 74% rename from dto/pricing.go rename to relaykit/dto/pricing.go index 1ed8dcd3..a32bd2a6 100644 --- a/dto/pricing.go +++ b/relaykit/dto/pricing.go @@ -1,14 +1,14 @@ package dto -import "github.com/QuantumNous/new-api/constant" +import "github.com/QuantumNous/new-api/relaykit/types" // 这里不好动就不动了,本来想独立出来的( type OpenAIModels struct { - Id string `json:"id"` - Object string `json:"object"` - Created int `json:"created"` - OwnedBy string `json:"owned_by"` - SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"` + Id string `json:"id"` + Object string `json:"object"` + Created int `json:"created"` + OwnedBy string `json:"owned_by"` + SupportedEndpointTypes []types.EndpointType `json:"supported_endpoint_types"` } type AnthropicModel struct { diff --git a/dto/ratio_sync.go b/relaykit/dto/ratio_sync.go similarity index 100% rename from dto/ratio_sync.go rename to relaykit/dto/ratio_sync.go diff --git a/dto/realtime.go b/relaykit/dto/realtime.go similarity index 98% rename from dto/realtime.go rename to relaykit/dto/realtime.go index 0fbfb86f..ac5dc644 100644 --- a/dto/realtime.go +++ b/relaykit/dto/realtime.go @@ -1,6 +1,6 @@ package dto -import "github.com/QuantumNous/new-api/types" +import "github.com/QuantumNous/new-api/relaykit/types" const ( RealtimeEventTypeError = "error" diff --git a/dto/request_common.go b/relaykit/dto/request_common.go similarity index 70% rename from dto/request_common.go rename to relaykit/dto/request_common.go index e6e40c3a..7796ef06 100644 --- a/dto/request_common.go +++ b/relaykit/dto/request_common.go @@ -1,13 +1,13 @@ package dto import ( - "github.com/QuantumNous/new-api/types" - "github.com/gin-gonic/gin" + "github.com/QuantumNous/new-api/relaykit/types" + "net/http" ) type Request interface { GetTokenCountMeta() *types.TokenCountMeta - IsStream(c *gin.Context) bool + IsStream(c *http.Request) bool SetModelName(modelName string) } @@ -19,7 +19,7 @@ func (b *BaseRequest) GetTokenCountMeta() *types.TokenCountMeta { TokenType: types.TokenTypeTokenizer, } } -func (b *BaseRequest) IsStream(c *gin.Context) bool { +func (b *BaseRequest) IsStream(c *http.Request) bool { return false } func (b *BaseRequest) SetModelName(modelName string) {} diff --git a/dto/rerank.go b/relaykit/dto/rerank.go similarity index 91% rename from dto/rerank.go rename to relaykit/dto/rerank.go index 96644368..a7315ef3 100644 --- a/dto/rerank.go +++ b/relaykit/dto/rerank.go @@ -2,10 +2,10 @@ package dto import ( "fmt" + "net/http" "strings" - "github.com/QuantumNous/new-api/types" - "github.com/gin-gonic/gin" + "github.com/QuantumNous/new-api/relaykit/types" ) type RerankRequest struct { @@ -18,7 +18,7 @@ type RerankRequest struct { OverLapTokens *int `json:"overlap_tokens,omitempty"` } -func (r *RerankRequest) IsStream(c *gin.Context) bool { +func (r *RerankRequest) IsStream(c *http.Request) bool { return false } diff --git a/dto/sensitive.go b/relaykit/dto/sensitive.go similarity index 100% rename from dto/sensitive.go rename to relaykit/dto/sensitive.go diff --git a/dto/user_settings.go b/relaykit/dto/user_settings.go similarity index 100% rename from dto/user_settings.go rename to relaykit/dto/user_settings.go diff --git a/dto/values.go b/relaykit/dto/values.go similarity index 100% rename from dto/values.go rename to relaykit/dto/values.go diff --git a/relaykit/go.mod b/relaykit/go.mod new file mode 100644 index 00000000..daed09ef --- /dev/null +++ b/relaykit/go.mod @@ -0,0 +1,24 @@ +module github.com/QuantumNous/new-api/relaykit + +go 1.25.1 + +require ( + github.com/google/uuid v1.6.0 + github.com/samber/lo v1.53.0 + github.com/shopspring/decimal v1.4.0 + github.com/stretchr/testify v1.11.1 + github.com/tidwall/gjson v1.19.0 + github.com/tidwall/sjson v1.2.5 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + golang.org/x/text v0.35.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/relaykit/go.sum b/relaykit/go.sum new file mode 100644 index 00000000..d4d8b57f --- /dev/null +++ b/relaykit/go.sum @@ -0,0 +1,40 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= +github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/relay/reasonmap/reasonmap.go b/relaykit/reasonmap/reasonmap.go similarity index 84% rename from relay/reasonmap/reasonmap.go rename to relaykit/reasonmap/reasonmap.go index 45b74bb1..8c6f66c6 100644 --- a/relay/reasonmap/reasonmap.go +++ b/relaykit/reasonmap/reasonmap.go @@ -3,7 +3,7 @@ package reasonmap import ( "strings" - "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/relaykit/types" ) func ClaudeStopReasonToOpenAIFinishReason(stopReason string) string { @@ -17,7 +17,7 @@ func ClaudeStopReasonToOpenAIFinishReason(stopReason string) string { case "tool_use": return "tool_calls" case "refusal": - return constant.FinishReasonContentFilter + return types.FinishReasonContentFilter default: return stopReason } @@ -31,7 +31,7 @@ func OpenAIFinishReasonToClaudeStopReason(finishReason string) string { return "stop_sequence" case "length", "max_tokens": return "max_tokens" - case constant.FinishReasonContentFilter: + case types.FinishReasonContentFilter: return "refusal" case "tool_calls": return "tool_use" diff --git a/relaykit/relayconvert/boundary_test.go b/relaykit/relayconvert/boundary_test.go new file mode 100644 index 00000000..49bedd72 --- /dev/null +++ b/relaykit/relayconvert/boundary_test.go @@ -0,0 +1,100 @@ +package relayconvert + +// boundary_test.go enforces the relaykit extraction dependency boundary +// (plans/relaykit-extraction-plan.md): packages that will move into the +// relaykit module must not grow imports of host-only packages. Entries in +// allowedViolations are the known couplings scheduled for removal in +// Phase 1/2 — shrink this list, never grow it. + +import ( + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + "testing" +) + +const modulePrefix = "github.com/QuantumNous/new-api/" + +// Packages (relative to the relaykit module root) covered by the boundary. +var kitDirs = []string{ + "relayconvert", + "dto", + "types", + "reasonmap", +} + +// Import prefixes forbidden inside the kit module: the entire host module +// (everything outside relaykit/) and gin. +var forbiddenPrefixes = []string{ + modulePrefix, + "github.com/gin-gonic/gin", +} + +// hostModuleExceptions are host-prefix imports that are actually the kit's +// own packages (the kit module path nests under the host path). +const kitModulePrefix = modulePrefix + "relaykit/" + +// Known pre-existing couplings, removed phase by phase. Key: "dir|import". +// All initial violations have been cleared; keep the map so future +// exemptions (if ever needed) are explicit and reviewed. +var allowedViolations = map[string]bool{} + +func TestRelaykitBoundary(t *testing.T) { + root := repoRoot(t) + fset := token.NewFileSet() + + for _, dir := range kitDirs { + err := filepath.WalkDir(filepath.Join(root, dir), func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { + return nil + } + file, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly) + if err != nil { + return err + } + for _, imp := range file.Imports { + importPath := strings.Trim(imp.Path.Value, `"`) + for _, prefix := range forbiddenPrefixes { + if importPath != prefix && !strings.HasPrefix(importPath, prefix) { + continue + } + if strings.HasPrefix(importPath, kitModulePrefix) { + continue + } + if allowedViolations[dir+"|"+importPath] { + continue + } + rel, _ := filepath.Rel(root, path) + t.Errorf("%s imports %q — forbidden inside future relaykit package %s (see plans/relaykit-extraction-plan.md)", rel, importPath, dir) + } + } + return nil + }) + if err != nil { + t.Fatalf("walking %s: %v", dir, err) + } + } +} + +func repoRoot(t *testing.T) string { + t.Helper() + dir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { + t.Fatal("go.mod not found above test directory") + } + dir = parent + } +} diff --git a/relaykit/relayconvert/claude_default_max_tokens_test.go b/relaykit/relayconvert/claude_default_max_tokens_test.go new file mode 100644 index 00000000..d6a3f9f3 --- /dev/null +++ b/relaykit/relayconvert/claude_default_max_tokens_test.go @@ -0,0 +1,111 @@ +package relayconvert + +import ( + "context" + "testing" + + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + sharedclaude "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/claude" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestClaudeDefaultMaxTokensPresence(t *testing.T) { + converters := []struct { + name string + convert func(t *testing.T, meta convmeta.Meta, clientMaxTokens *uint) (*dto.ClaudeRequest, error) + }{ + { + name: "chat completions", + convert: func(t *testing.T, meta convmeta.Meta, clientMaxTokens *uint) (*dto.ClaudeRequest, error) { + t.Helper() + return OpenAIChatRequestToClaudeMessages(context.Background(), meta, dto.GeneralOpenAIRequest{ + Model: "claude-test", + MaxTokens: clientMaxTokens, + Messages: []dto.Message{ + {Role: "user", Content: "hello"}, + }, + }) + }, + }, + { + name: "responses", + convert: func(t *testing.T, meta convmeta.Meta, clientMaxTokens *uint) (*dto.ClaudeRequest, error) { + t.Helper() + return OpenAIResponsesRequestToClaudeMessages(context.Background(), meta, &dto.OpenAIResponsesRequest{ + Model: "claude-test", + Input: []byte(`"hello"`), + MaxOutputTokens: clientMaxTokens, + }) + }, + }, + } + + for _, converter := range converters { + t.Run(converter.name, func(t *testing.T) { + t.Run("callback absent fails conversion", func(t *testing.T) { + got, err := converter.convert(t, &convmeta.Values{}, nil) + require.ErrorIs(t, err, sharedclaude.ErrMissingMaxTokens) + assert.Nil(t, got) + }) + + t.Run("callback absent, client value wins", func(t *testing.T) { + clientMaxTokens := uint(99) + got, err := converter.convert(t, &convmeta.Values{}, &clientMaxTokens) + require.NoError(t, err) + require.NotNil(t, got.MaxTokens) + assert.Equal(t, clientMaxTokens, *got.MaxTokens) + }) + + t.Run("configured zero", func(t *testing.T) { + got, err := converter.convert(t, claudeDefaultsMeta(func(string) int { return 0 }), nil) + require.NoError(t, err) + require.NotNil(t, got.MaxTokens) + assert.Zero(t, *got.MaxTokens) + }) + + t.Run("configured positive", func(t *testing.T) { + got, err := converter.convert(t, claudeDefaultsMeta(func(string) int { return 512 }), nil) + require.NoError(t, err) + require.NotNil(t, got.MaxTokens) + assert.Equal(t, uint(512), *got.MaxTokens) + }) + + t.Run("client nonzero wins", func(t *testing.T) { + clientMaxTokens := uint(99) + got, err := converter.convert(t, claudeDefaultsMeta(func(string) int { return 512 }), &clientMaxTokens) + require.NoError(t, err) + require.NotNil(t, got.MaxTokens) + assert.Equal(t, clientMaxTokens, *got.MaxTokens) + }) + }) + } +} + +// The thinking adapter's max_tokens floor is an injection path of its own: a +// "-thinking" request without max_tokens must keep converting even when no +// DefaultMaxTokens hook is configured. +func TestClaudeThinkingAdapterSatisfiesMaxTokensWithoutCallback(t *testing.T) { + meta := &convmeta.Values{Options: &convmeta.Options{ + Claude: convmeta.ClaudeOptions{ + ThinkingAdapterEnabled: true, + ThinkingAdapterBudgetTokensPercentage: 0.8, + }, + }} + got, err := OpenAIChatRequestToClaudeMessages(context.Background(), meta, dto.GeneralOpenAIRequest{ + Model: "claude-test-thinking", + Messages: []dto.Message{ + {Role: "user", Content: "hello"}, + }, + }) + require.NoError(t, err) + require.NotNil(t, got.MaxTokens) + assert.Equal(t, uint(1280), *got.MaxTokens) +} + +func claudeDefaultsMeta(defaultMaxTokens func(string) int) convmeta.Meta { + return &convmeta.Values{Options: &convmeta.Options{ + Claude: convmeta.ClaudeOptions{DefaultMaxTokens: defaultMaxTokens}, + }} +} diff --git a/relaykit/relayconvert/convmeta/format.go b/relaykit/relayconvert/convmeta/format.go new file mode 100644 index 00000000..ab8bf018 --- /dev/null +++ b/relaykit/relayconvert/convmeta/format.go @@ -0,0 +1,31 @@ +package convmeta + +import ( + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" +) + +// GuessRelayFormatFromRequest infers the relay format from a request DTO's +// concrete type. Moved from relay/common (which keeps a delegating alias). +func GuessRelayFormatFromRequest(req any) (types.RelayFormat, bool) { + switch req.(type) { + case *dto.GeneralOpenAIRequest, dto.GeneralOpenAIRequest: + return types.RelayFormatOpenAI, true + case *dto.OpenAIResponsesRequest, dto.OpenAIResponsesRequest: + return types.RelayFormatOpenAIResponses, true + case *dto.ClaudeRequest, dto.ClaudeRequest: + return types.RelayFormatClaude, true + case *dto.GeminiChatRequest, dto.GeminiChatRequest: + return types.RelayFormatGemini, true + case *dto.EmbeddingRequest, dto.EmbeddingRequest: + return types.RelayFormatEmbedding, true + case *dto.RerankRequest, dto.RerankRequest: + return types.RelayFormatRerank, true + case *dto.ImageRequest, dto.ImageRequest: + return types.RelayFormatOpenAIImage, true + case *dto.AudioRequest, dto.AudioRequest: + return types.RelayFormatOpenAIAudio, true + default: + return "", false + } +} diff --git a/relaykit/relayconvert/convmeta/meta.go b/relaykit/relayconvert/convmeta/meta.go new file mode 100644 index 00000000..68848af9 --- /dev/null +++ b/relaykit/relayconvert/convmeta/meta.go @@ -0,0 +1,215 @@ +// Package convmeta defines the conversion-context contract between format +// converters (future relaykit) and the hosting application. Converters read +// protocol state and per-request options exclusively through the Meta +// interface; the host's RelayInfo implements it. +package convmeta + +import ( + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" +) + +// Meta is the only view of the relay session that format converters may use. +// It is satisfied by *relaycommon.RelayInfo on the host side; other embedders +// (tests, external relaykit users) can use *Values. +// Implementations backed by pointer types must make every method safe on a nil +// receiver: a typed-nil pointer stored in Meta is still a non-nil interface, +// and relaykit deliberately does not use reflection to detect that case. +type Meta interface { + GetOriginModelName() string + GetUpstreamModelName() string + // HasChannelMeta reports whether upstream channel information is attached; + // converters use it to decide if GetUpstreamModelName is meaningful. + HasChannelMeta() bool + GetChannelID() int + GetChannelType() int + GetIsStream() bool + GetReasoningEffort() string + // SetReasoningEffort records the effort level a converter derived from a + // model-name suffix so downstream billing/logging can see it. + SetReasoningEffort(effort string) + GetEstimatePromptTokens() int + + // EnsureClaudeConvertInfo lazily creates and returns the mutable + // OpenAI→Claude stream conversion state. For non-nil receivers, the same + // instance must be returned for the lifetime of one streaming session; a + // nil receiver may return a temporary initialized state. + EnsureClaudeConvertInfo() *ClaudeConvertInfo + + // GetSendResponseCount / IncrSendResponseCount expose the shared + // downstream-chunk counter (the host may also increment it). + GetSendResponseCount() int + IncrSendResponseCount() + + // AppendRequestConversion records a hop in the request format chain. + AppendRequestConversion(format types.RelayFormat) + + // ConvOptions returns the request-scoped conversion options snapshot. + // Must never return nil. + ConvOptions() *Options +} + +// ClaudeConvertInfo carries mutable state for OpenAI chat → Claude Messages +// stream conversion. Moved here from relay/common (which keeps an alias). +type ClaudeConvertInfo struct { + LastMessagesType string + Index int + Usage *dto.Usage + FinishReason string + Done bool + + ToolCallBaseIndex int + ToolCallMaxIndexOffset int +} + +const ( + LastMessageTypeNone = "none" + LastMessageTypeText = "text" + LastMessageTypeTools = "tools" + LastMessageTypeThinking = "thinking" +) + +// Values is a plain-struct Meta implementation for tests and non-RelayInfo +// hosts (the relaykit-native entry point). +type Values struct { + OriginModelName string + UpstreamModelName string + ChannelMetaAttached bool + ChannelID int + ChannelType int + IsStream bool + ReasoningEffort string + EstimatePromptTokens int + + ClaudeConvertInfo *ClaudeConvertInfo + SendResponseCount int + ConversionChain []types.RelayFormat + + Options *Options +} + +var _ Meta = (*Values)(nil) + +func (v *Values) GetOriginModelName() string { + if v == nil { + return "" + } + return v.OriginModelName +} + +func (v *Values) GetUpstreamModelName() string { + if v == nil { + return "" + } + return v.UpstreamModelName +} + +func (v *Values) HasChannelMeta() bool { + return v != nil && v.ChannelMetaAttached +} + +func (v *Values) GetChannelID() int { + if v == nil { + return 0 + } + return v.ChannelID +} + +func (v *Values) GetChannelType() int { + if v == nil { + return 0 + } + return v.ChannelType +} + +func (v *Values) GetIsStream() bool { + return v != nil && v.IsStream +} + +func (v *Values) GetReasoningEffort() string { + if v == nil { + return "" + } + return v.ReasoningEffort +} + +func (v *Values) SetReasoningEffort(effort string) { + if v != nil { + v.ReasoningEffort = effort + } +} + +func (v *Values) GetEstimatePromptTokens() int { + if v == nil { + return 0 + } + return v.EstimatePromptTokens +} + +func (v *Values) EnsureClaudeConvertInfo() *ClaudeConvertInfo { + if v == nil { + return &ClaudeConvertInfo{LastMessagesType: LastMessageTypeNone} + } + if v.ClaudeConvertInfo == nil { + v.ClaudeConvertInfo = &ClaudeConvertInfo{LastMessagesType: LastMessageTypeNone} + } + return v.ClaudeConvertInfo +} + +func (v *Values) GetSendResponseCount() int { + if v == nil { + return 0 + } + return v.SendResponseCount +} + +func (v *Values) IncrSendResponseCount() { + if v != nil { + v.SendResponseCount++ + } +} + +func (v *Values) AppendRequestConversion(format types.RelayFormat) { + if v == nil || format == "" { + return + } + if n := len(v.ConversionChain); n > 0 && v.ConversionChain[n-1] == format { + return + } + v.ConversionChain = append(v.ConversionChain, format) +} + +func (v *Values) ConvOptions() *Options { + if v == nil { + return &Options{} + } + if v.Options == nil { + v.Options = &Options{} + } + return v.Options +} + +// UpstreamModelName / ChannelTypeOf are nil-safe accessors for optional Meta +// values (converters are often called with a nil Meta in tests and compat +// shims). +func UpstreamModelName(m Meta) string { + if m == nil || !m.HasChannelMeta() { + return "" + } + return m.GetUpstreamModelName() +} + +func ChannelTypeOf(m Meta) int { + if m == nil || !m.HasChannelMeta() { + return 0 + } + return m.GetChannelType() +} + +// OptionsOf returns m's conversion options, or empty defaults when m is nil. +func OptionsOf(m Meta) *Options { + if m == nil { + return &Options{} + } + return m.ConvOptions() +} diff --git a/relaykit/relayconvert/convmeta/meta_test.go b/relaykit/relayconvert/convmeta/meta_test.go new file mode 100644 index 00000000..0b055f3f --- /dev/null +++ b/relaykit/relayconvert/convmeta/meta_test.go @@ -0,0 +1,38 @@ +package convmeta + +import ( + "testing" + + "github.com/QuantumNous/new-api/relaykit/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValuesTypedNilMetaIsSafe(t *testing.T) { + var values *Values + var meta Meta = values + + assert.Empty(t, meta.GetOriginModelName()) + assert.Empty(t, meta.GetUpstreamModelName()) + assert.False(t, meta.HasChannelMeta()) + assert.Zero(t, meta.GetChannelID()) + assert.Zero(t, meta.GetChannelType()) + assert.False(t, meta.GetIsStream()) + assert.Empty(t, meta.GetReasoningEffort()) + assert.Zero(t, meta.GetEstimatePromptTokens()) + assert.Zero(t, meta.GetSendResponseCount()) + + require.NotPanics(t, func() { + meta.SetReasoningEffort("high") + meta.IncrSendResponseCount() + meta.AppendRequestConversion(types.RelayFormatClaude) + }) + + convertInfo := meta.EnsureClaudeConvertInfo() + require.NotNil(t, convertInfo) + assert.Equal(t, LastMessageTypeNone, convertInfo.LastMessagesType) + require.NotNil(t, meta.ConvOptions()) + require.NotNil(t, OptionsOf(meta)) + assert.Empty(t, UpstreamModelName(meta)) + assert.Zero(t, ChannelTypeOf(meta)) +} diff --git a/relaykit/relayconvert/convmeta/options.go b/relaykit/relayconvert/convmeta/options.go new file mode 100644 index 00000000..af8cfeb6 --- /dev/null +++ b/relaykit/relayconvert/convmeta/options.go @@ -0,0 +1,79 @@ +package convmeta + +// Options is the per-request snapshot of host configuration that converters +// consult. The host fills it from its settings system when constructing the +// Meta (see relaycommon.RelayInfo.ConvOptions); relaykit users fill it +// directly. Zero value = every adaptation disabled, no defaults applied. +type Options struct { + Claude ClaudeOptions + Gemini GeminiOptions + + // OpenRouterDialect marks the upstream as OpenRouter's OpenAI-compatible + // surface, which accepts extra fields (reasoning config, cache_control on + // system parts) that converters emit only for that dialect. The host sets + // it from the channel type. + OpenRouterDialect bool + + // PreserveThinkingSuffix reports models whose -thinking/-nothinking/effort + // suffix must be kept on the outgoing model name (host blacklist lookup). + // Nil means "never preserve". + PreserveThinkingSuffix func(modelName string) bool +} + +type ClaudeOptions struct { + // ThinkingAdapterEnabled turns "-thinking"-suffixed OpenAI model names + // into Claude extended-thinking requests. + ThinkingAdapterEnabled bool + // ThinkingAdapterBudgetTokensPercentage sizes thinking budget_tokens as a + // fraction of max_tokens when the adapter fires. + ThinkingAdapterBudgetTokensPercentage float64 + // DefaultMaxTokens returns the max_tokens to inject when the source + // request carries none. The Claude Messages API requires max_tokens + // (omitting it is a 400), so when this hook is nil and no other path + // supplies a value, OpenAI→Claude request conversion fails with an + // explicit error instead of emitting a request the upstream is + // guaranteed to reject. The new-api host always provides this hook; + // standalone relaykit users must supply one or guarantee max_tokens on + // every request. + DefaultMaxTokens func(modelName string) int +} + +type GeminiOptions struct { + // ThinkingAdapterEnabled maps -thinking/-nothinking/effort suffixes to + // Gemini thinkingConfig. + ThinkingAdapterEnabled bool + // ThinkingAdapterBudgetTokensPercentage sizes thinkingBudget as a fraction + // of maxOutputTokens when the adapter fires. + ThinkingAdapterBudgetTokensPercentage float64 + // FunctionCallThoughtSignatureEnabled attaches thoughtSignature bypass + // values to function-call parts. + FunctionCallThoughtSignatureEnabled bool + // SupportsImagine reports whether the model supports image generation + // (switches response modalities). Nil means "never". + SupportsImagine func(modelName string) bool + // SafetySetting returns the harm threshold for a category. Nil or empty + // return means no safetySettings are attached. + SafetySetting func(category string) string +} + +func (o *ClaudeOptions) DefaultMaxTokensFor(modelName string) (int, bool) { + if o == nil || o.DefaultMaxTokens == nil { + return 0, false + } + return o.DefaultMaxTokens(modelName), true +} + +func (o *GeminiOptions) SupportsImagineModel(modelName string) bool { + return o != nil && o.SupportsImagine != nil && o.SupportsImagine(modelName) +} + +func (o *GeminiOptions) SafetySettingFor(category string) string { + if o == nil || o.SafetySetting == nil { + return "" + } + return o.SafetySetting(category) +} + +func (o *Options) ShouldPreserveThinkingSuffix(modelName string) bool { + return o != nil && o.PreserveThinkingSuffix != nil && o.PreserveThinkingSuffix(modelName) +} diff --git a/relaykit/relayconvert/golden_test.go b/relaykit/relayconvert/golden_test.go new file mode 100644 index 00000000..112ba268 --- /dev/null +++ b/relaykit/relayconvert/golden_test.go @@ -0,0 +1,475 @@ +package relayconvert + +// golden_test.go pins the byte-level output of every registered (from, to) +// conversion route so the relaykit extraction refactor can prove behavior is +// unchanged at each phase. Run with -update to regenerate testdata/golden. +// +// Volatile values (generated UUID-based ids, unix timestamps) are normalized +// before comparison so the snapshots are deterministic. + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + "github.com/QuantumNous/new-api/relaykit/types" + "github.com/stretchr/testify/require" +) + +var updateGolden = flag.Bool("update", false, "update golden files") + +// TestMain installs a deterministic media resolver so image-bearing fixtures +// convert without network access. +func TestMain(m *testing.M) { + flag.Parse() + SetMediaResolver(MediaResolver{ + GetBase64Data: func(c context.Context, source types.FileSource, reason ...string) (string, string, error) { + return "aGVsbG8=", "image/png", nil + }, + DecodeBase64FileData: func(base64String string) (string, string, error) { + return "aGVsbG8=", "image/png", nil + }, + }) + os.Exit(m.Run()) +} + +const goldenDir = "testdata/golden" + +var ( + hex32Re = regexp.MustCompile(`[0-9a-f]{32}`) + timestampRe = regexp.MustCompile(`("created(_at)?"\s*:\s*)\d{9,}`) +) + +func normalizeVolatile(data []byte) []byte { + data = hex32Re.ReplaceAll(data, []byte("")) + data = timestampRe.ReplaceAll(data, []byte(`${1}0`)) + return data +} + +func marshalGolden(t *testing.T, value any) []byte { + t.Helper() + data, err := json.MarshalIndent(value, "", " ") + require.NoError(t, err) + return append(normalizeVolatile(data), '\n') +} + +func checkGolden(t *testing.T, name string, got []byte) { + t.Helper() + path := filepath.Join(goldenDir, name+".golden.json") + if *updateGolden { + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) + require.NoError(t, os.WriteFile(path, got, 0o644)) + return + } + want, err := os.ReadFile(path) + require.NoError(t, err, "golden file missing, run: go test ./service/relayconvert -run TestGolden -update") + require.Equal(t, string(want), string(got), "conversion output drifted from golden snapshot %s", path) +} + +// goldenInfo mirrors the host's default converter options (new-api's +// model_setting defaults at the time the snapshots were recorded) so the +// golden files stay comparable across the extraction. +func goldenInfo() convmeta.Meta { + return &convmeta.Values{ + ChannelMetaAttached: true, + UpstreamModelName: "upstream-model", + ClaudeConvertInfo: &convmeta.ClaudeConvertInfo{ + LastMessagesType: convmeta.LastMessageTypeNone, + }, + Options: &convmeta.Options{ + Gemini: convmeta.GeminiOptions{ + ThinkingAdapterBudgetTokensPercentage: 0.6, + FunctionCallThoughtSignatureEnabled: true, + SafetySetting: func(string) string { return "OFF" }, + }, + }, + } +} + +// --------------------------------------------------------------------------- +// Fixtures: one representative rich request per source format +// --------------------------------------------------------------------------- + +// Fixtures are built by unmarshalling wire-format JSON into the dto types — +// the same path production requests take — so they stay valid as struct +// internals evolve. +func fixtureRequests() map[types.RelayFormat]any { + openai := &dto.GeneralOpenAIRequest{} + mustUnmarshalFixture(`{ + "model": "gpt-test", + "max_tokens": 1024, + "stream": true, + "messages": [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": [ + {"type": "text", "text": "What is in this image?"}, + {"type": "image_url", "image_url": {"url": "https://example.com/cat.png", "detail": "high"}} + ]}, + {"role": "assistant", "tool_calls": [{"id": "call_abc", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}}]}, + {"role": "tool", "tool_call_id": "call_abc", "content": "15 degrees"}, + {"role": "user", "content": "Summarize."} + ], + "tools": [{"type": "function", "function": {"name": "get_weather", "description": "Get weather by city", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}}], + "tool_choice": "auto" + }`, openai) + + claude := &dto.ClaudeRequest{} + mustUnmarshalFixture(`{ + "model": "claude-test", + "max_tokens": 1024, + "stream": true, + "system": "You are a helpful assistant.", + "messages": [ + {"role": "user", "content": [ + {"type": "text", "text": "What is in this image?"}, + {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "aGVsbG8="}} + ]}, + {"role": "assistant", "content": [ + {"type": "thinking", "thinking": "Let me look.", "signature": "sig"}, + {"type": "tool_use", "id": "toolu_abc", "name": "get_weather", "input": {"city": "Paris"}} + ]}, + {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_abc", "content": "15 degrees"}]} + ], + "tools": [{"name": "get_weather", "description": "Get weather by city", "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}], + "thinking": {"type": "enabled", "budget_tokens": 512} + }`, claude) + + gemini := &dto.GeminiChatRequest{} + mustUnmarshalFixture(`{ + "contents": [ + {"role": "user", "parts": [ + {"text": "What is in this image?"}, + {"inlineData": {"mimeType": "image/png", "data": "aGVsbG8="}} + ]}, + {"role": "model", "parts": [{"functionCall": {"name": "get_weather", "args": {"city": "Paris"}}}]}, + {"role": "user", "parts": [{"functionResponse": {"name": "get_weather", "response": {"result": "15 degrees"}}}]} + ], + "systemInstruction": {"parts": [{"text": "You are a helpful assistant."}]}, + "tools": [{"functionDeclarations": [{"name": "get_weather", "description": "Get weather by city", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}]}], + "generationConfig": {"maxOutputTokens": 1024, "temperature": 0.7} + }`, gemini) + + responses := &dto.OpenAIResponsesRequest{} + mustUnmarshalFixture(`{ + "model": "gpt-test", + "stream": true, + "max_output_tokens": 1024, + "instructions": "You are a helpful assistant.", + "input": [ + {"type": "message", "role": "user", "content": [ + {"type": "input_text", "text": "What is in this image?"}, + {"type": "input_image", "image_url": "https://example.com/cat.png"} + ]}, + {"type": "function_call", "call_id": "call_abc", "name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}, + {"type": "function_call_output", "call_id": "call_abc", "output": "15 degrees"} + ], + "tools": [{"type": "function", "name": "get_weather", "description": "Get weather by city", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}] + }`, responses) + + return map[types.RelayFormat]any{ + types.RelayFormatOpenAI: openai, + types.RelayFormatClaude: claude, + types.RelayFormatGemini: gemini, + types.RelayFormatOpenAIResponses: responses, + } +} + +// --------------------------------------------------------------------------- +// Fixtures: one representative non-stream response per source format +// --------------------------------------------------------------------------- + +func fixtureResponses() map[types.RelayFormat]any { + openai := &dto.OpenAITextResponse{} + mustUnmarshalFixture(`{ + "id": "chatcmpl-fixed", + "object": "chat.completion", + "created": 1700000000, + "model": "gpt-test", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "The answer is 42.", + "reasoning_content": "Deep thought.", + "tool_calls": [{"id": "call_abc", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}}] + }, + "finish_reason": "tool_calls" + }], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "prompt_tokens_details": {"cached_tokens": 3}, + "completion_tokens_details": {"reasoning_tokens": 2} + } + }`, openai) + + claude := &dto.ClaudeResponse{} + mustUnmarshalFixture(`{ + "id": "msg_fixed", + "type": "message", + "role": "assistant", + "model": "claude-test", + "content": [ + {"type": "text", "text": "The answer is 42."}, + {"type": "tool_use", "id": "toolu_abc", "name": "get_weather", "input": {"city": "Paris"}} + ], + "stop_reason": "tool_use", + "usage": {"input_tokens": 10, "output_tokens": 5, "cache_read_input_tokens": 3, "cache_creation_input_tokens": 2} + }`, claude) + + gemini := &dto.GeminiChatResponse{} + mustUnmarshalFixture(`{ + "candidates": [{ + "finishReason": "STOP", + "content": { + "role": "model", + "parts": [ + {"text": "The answer is 42."}, + {"functionCall": {"name": "get_weather", "args": {"city": "Paris"}}} + ] + } + }], + "usageMetadata": {"promptTokenCount": 10, "candidatesTokenCount": 5, "thoughtsTokenCount": 2, "totalTokenCount": 15} + }`, gemini) + + responses := &dto.OpenAIResponsesResponse{} + mustUnmarshalFixture(`{ + "id": "resp_fixed", + "object": "response", + "model": "gpt-test", + "status": "completed", + "output": [ + {"type": "reasoning", "summary": [{"type": "summary_text", "text": "Deep thought."}]}, + {"type": "message", "role": "assistant", "status": "completed", "content": [{"type": "output_text", "text": "The answer is 42."}]}, + {"type": "function_call", "call_id": "call_abc", "name": "get_weather", "arguments": "{\"city\":\"Paris\"}", "status": "completed"} + ], + "usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15} + }`, responses) + + return map[types.RelayFormat]any{ + types.RelayFormatOpenAI: openai, + types.RelayFormatClaude: claude, + types.RelayFormatGemini: gemini, + types.RelayFormatOpenAIResponses: responses, + } +} + +// --------------------------------------------------------------------------- +// Fixtures: stream chunk sequences per source format +// --------------------------------------------------------------------------- + +func fixtureStreamChunks() map[types.RelayFormat][]any { + return map[types.RelayFormat][]any{ + types.RelayFormatOpenAI: { + chatStreamChunk(`{"id":"chatcmpl-fixed","object":"chat.completion.chunk","created":1700000000,"model":"gpt-test","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"}}]}`), + chatStreamChunk(`{"id":"chatcmpl-fixed","object":"chat.completion.chunk","created":1700000000,"model":"gpt-test","choices":[{"index":0,"delta":{"content":" world"}}]}`), + chatStreamChunk(`{"id":"chatcmpl-fixed","object":"chat.completion.chunk","created":1700000000,"model":"gpt-test","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":4,"completion_tokens":2,"total_tokens":6}}`), + }, + types.RelayFormatClaude: { + claudeStreamChunk(`{"type":"message_start","message":{"id":"msg_fixed","type":"message","role":"assistant","model":"claude-test","content":[],"usage":{"input_tokens":4,"output_tokens":0}}}`), + claudeStreamChunk(`{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`), + claudeStreamChunk(`{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello world"}}`), + claudeStreamChunk(`{"type":"content_block_stop","index":0}`), + claudeStreamChunk(`{"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":2}}`), + claudeStreamChunk(`{"type":"message_stop"}`), + }, + types.RelayFormatGemini: { + geminiStreamChunk(`{"candidates":[{"content":{"role":"model","parts":[{"text":"Hello"}]}}]}`), + geminiStreamChunk(`{"candidates":[{"finishReason":"STOP","content":{"role":"model","parts":[{"text":" world"}]}}],"usageMetadata":{"promptTokenCount":4,"candidatesTokenCount":2,"totalTokenCount":6}}`), + }, + types.RelayFormatOpenAIResponses: { + responsesStreamChunk(`{"type":"response.output_text.delta","delta":"Hello"}`), + responsesStreamChunk(`{"type":"response.output_text.delta","delta":" world"}`), + responsesStreamChunk(`{"type":"response.completed","response":{"id":"resp_fixed","object":"response","status":"completed","model":"gpt-test","usage":{"input_tokens":4,"output_tokens":2,"total_tokens":6}}}`), + }, + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +func allFormats() []types.RelayFormat { + return []types.RelayFormat{ + types.RelayFormatOpenAI, + types.RelayFormatClaude, + types.RelayFormatGemini, + types.RelayFormatOpenAIResponses, + } +} + +func TestGoldenRequestConversionMatrix(t *testing.T) { + requests := fixtureRequests() + for _, from := range allFormats() { + for _, to := range allFormats() { + if from == to { + continue + } + if _, ok := lookupRequestRoute(from, to); !ok { + t.Fatalf("request route %s -> %s is no longer registered", from, to) + } + name := fmt.Sprintf("request/%s_to_%s", from, to) + t.Run(name, func(t *testing.T) { + result, err := ConvertRequest(nil, goldenInfo(), to, deepCopyFixture(t, requests[from])) + require.NoError(t, err) + checkGolden(t, name, marshalGolden(t, result.Value)) + }) + } + } +} + +func TestGoldenResponseConversionMatrix(t *testing.T) { + responses := fixtureResponses() + for _, from := range allFormats() { + for _, to := range allFormats() { + if from == to { + continue + } + name := fmt.Sprintf("response/%s_to_%s", from, to) + t.Run(name, func(t *testing.T) { + result, err := ConvertResponse(nil, goldenInfo(), to, deepCopyFixture(t, responses[from])) + require.NoError(t, err) + checkGolden(t, name, marshalGolden(t, result.Value)) + }) + } + } +} + +func TestGoldenStreamConversionMatrix(t *testing.T) { + chunkSets := fixtureStreamChunks() + for _, from := range allFormats() { + for _, to := range allFormats() { + if from == to { + continue + } + name := fmt.Sprintf("stream/%s_to_%s", from, to) + t.Run(name, func(t *testing.T) { + info := goldenInfo() + state, err := NewResponseStreamState(from, to, ResponseStreamOptions{ + ID: "stream_fixed", + Model: "stream-model", + }) + require.NoError(t, err) + + var outputs []any + for _, chunk := range chunkSets[from] { + results, err := ConvertStreamResponseChunk(nil, info, state, deepCopyFixture(t, chunk)) + require.NoError(t, err) + for _, r := range results { + outputs = append(outputs, r.Value) + } + } + finals, err := FinalizeStreamResponse(nil, info, state) + require.NoError(t, err) + for _, r := range finals { + outputs = append(outputs, r.Value) + } + + snapshot := map[string]any{ + "events": outputs, + "usage": state.Usage(), + } + checkGolden(t, name, marshalGolden(t, snapshot)) + }) + } + } +} + +// --------------------------------------------------------------------------- +// Fixture helpers +// --------------------------------------------------------------------------- + +func rawJSON(s string) json.RawMessage { + return json.RawMessage(s) +} + +// deepCopyFixture guards against converters mutating shared fixture state +// between subtests (JSON round-trip through the concrete type). +func deepCopyFixture(t *testing.T, v any) any { + t.Helper() + data, err := json.Marshal(v) + require.NoError(t, err) + switch v.(type) { + case *dto.GeneralOpenAIRequest: + out := &dto.GeneralOpenAIRequest{} + require.NoError(t, json.Unmarshal(data, out)) + return out + case *dto.ClaudeRequest: + out := &dto.ClaudeRequest{} + require.NoError(t, json.Unmarshal(data, out)) + return out + case *dto.GeminiChatRequest: + out := &dto.GeminiChatRequest{} + require.NoError(t, json.Unmarshal(data, out)) + return out + case *dto.OpenAIResponsesRequest: + out := &dto.OpenAIResponsesRequest{} + require.NoError(t, json.Unmarshal(data, out)) + return out + case *dto.OpenAITextResponse: + out := &dto.OpenAITextResponse{} + require.NoError(t, json.Unmarshal(data, out)) + return out + case *dto.ClaudeResponse: + out := &dto.ClaudeResponse{} + require.NoError(t, json.Unmarshal(data, out)) + return out + case *dto.GeminiChatResponse: + out := &dto.GeminiChatResponse{} + require.NoError(t, json.Unmarshal(data, out)) + return out + case *dto.OpenAIResponsesResponse: + out := &dto.OpenAIResponsesResponse{} + require.NoError(t, json.Unmarshal(data, out)) + return out + case *dto.ChatCompletionsStreamResponse: + out := &dto.ChatCompletionsStreamResponse{} + require.NoError(t, json.Unmarshal(data, out)) + return out + case *dto.ResponsesStreamResponse: + out := &dto.ResponsesStreamResponse{} + require.NoError(t, json.Unmarshal(data, out)) + return out + default: + t.Fatalf("deepCopyFixture: unsupported fixture type %T", v) + return nil + } +} + +func chatStreamChunk(raw string) *dto.ChatCompletionsStreamResponse { + var r dto.ChatCompletionsStreamResponse + mustUnmarshalFixture(raw, &r) + return &r +} + +func claudeStreamChunk(raw string) *dto.ClaudeResponse { + var r dto.ClaudeResponse + mustUnmarshalFixture(raw, &r) + return &r +} + +func geminiStreamChunk(raw string) *dto.GeminiChatResponse { + var r dto.GeminiChatResponse + mustUnmarshalFixture(raw, &r) + return &r +} + +func responsesStreamChunk(raw string) *dto.ResponsesStreamResponse { + var r dto.ResponsesStreamResponse + mustUnmarshalFixture(raw, &r) + return &r +} + +func mustUnmarshalFixture(raw string, out any) { + if err := json.Unmarshal([]byte(raw), out); err != nil { + panic(fmt.Sprintf("bad fixture JSON: %v", err)) + } +} diff --git a/service/relayconvert/internal/claude_messages/to_oai_chat_req.go b/relaykit/relayconvert/internal/claude_messages/to_oai_chat_req.go similarity index 83% rename from service/relayconvert/internal/claude_messages/to_oai_chat_req.go rename to relaykit/relayconvert/internal/claude_messages/to_oai_chat_req.go index e7ef305c..56e6663e 100644 --- a/service/relayconvert/internal/claude_messages/to_oai_chat_req.go +++ b/relaykit/relayconvert/internal/claude_messages/to_oai_chat_req.go @@ -4,11 +4,9 @@ import ( "fmt" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" - relaymeta "github.com/QuantumNous/new-api/service/relayconvert/internal/meta" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" ) const ( @@ -24,28 +22,28 @@ type openRouterRequestReasoning struct { Exclude bool `json:"exclude,omitempty"` } -func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info *relaycommon.RelayInfo) (*dto.GeneralOpenAIRequest, error) { +func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info convmeta.Meta) (*dto.GeneralOpenAIRequest, error) { openAIRequest := dto.GeneralOpenAIRequest{ Model: claudeRequest.Model, Temperature: claudeRequest.Temperature, } if claudeRequest.MaxTokens != nil { - openAIRequest.MaxTokens = common.GetPointer(*claudeRequest.MaxTokens) + openAIRequest.MaxTokens = kitutil.GetPointer(*claudeRequest.MaxTokens) } if claudeRequest.TopP != nil { - openAIRequest.TopP = common.GetPointer(*claudeRequest.TopP) + openAIRequest.TopP = kitutil.GetPointer(*claudeRequest.TopP) } if claudeRequest.TopK != nil { - openAIRequest.TopK = common.GetPointer(*claudeRequest.TopK) + openAIRequest.TopK = kitutil.GetPointer(*claudeRequest.TopK) } if claudeRequest.Stream != nil { - openAIRequest.Stream = common.GetPointer(*claudeRequest.Stream) + openAIRequest.Stream = kitutil.GetPointer(*claudeRequest.Stream) } - isOpenRouter := relaymeta.RelayInfoChannelType(info) == constant.ChannelTypeOpenRouter + isOpenRouter := convmeta.OptionsOf(info).OpenRouterDialect if isOpenRouter { if effort := claudeRequest.GetEfforts(); effort != "" { - effortBytes, _ := common.Marshal(effort) + effortBytes, _ := kitutil.Marshal(effort) openAIRequest.Verbosity = effortBytes } if claudeRequest.Thinking != nil { @@ -60,7 +58,7 @@ func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info *re Enabled: true, } } - reasoningJSON, err := common.Marshal(reasoningConfig) + reasoningJSON, err := kitutil.Marshal(reasoningConfig) if err != nil { return nil, fmt.Errorf("failed to marshal reasoning: %w", err) } @@ -68,7 +66,7 @@ func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info *re } } else if info != nil { thinkingSuffix := "-thinking" - if strings.HasSuffix(info.OriginModelName, thinkingSuffix) && + if strings.HasSuffix(info.GetOriginModelName(), thinkingSuffix) && !strings.HasSuffix(openAIRequest.Model, thinkingSuffix) { openAIRequest.Model = openAIRequest.Model + thinkingSuffix } @@ -80,7 +78,7 @@ func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info *re openAIRequest.Stop = claudeRequest.StopSequences } - tools, _ := common.Any2Type[[]dto.Tool](claudeRequest.Tools) + tools, _ := kitutil.Any2Type[[]dto.Tool](claudeRequest.Tools) openAITools := make([]dto.ToolCallRequest, 0) for _, claudeTool := range tools { openAITool := dto.ToolCallRequest{ @@ -109,7 +107,7 @@ func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info *re openAIMessage := dto.Message{ Role: "system", } - isOpenRouterClaude := isOpenRouter && strings.HasPrefix(relaymeta.RelayInfoUpstreamModelName(info), "anthropic/claude") + isOpenRouterClaude := isOpenRouter && strings.HasPrefix(convmeta.UpstreamModelName(info), "anthropic/claude") if isOpenRouterClaude { systemMediaMessages := make([]dto.MediaContent, 0, len(systems)) for _, system := range systems { @@ -189,7 +187,7 @@ func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info *re oaiToolMessage.SetStringContent(mediaMsg.GetStringContent()) } else { mediaContents := mediaMsg.ParseMediaContent() - encodedJSON, _ := common.Marshal(mediaContents) + encodedJSON, _ := kitutil.Marshal(mediaContents) oaiToolMessage.SetStringContent(string(encodedJSON)) } openAIMessages = append(openAIMessages, oaiToolMessage) @@ -213,7 +211,7 @@ func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info *re } func requestToJSONString(v interface{}) string { - b, err := common.Marshal(v) + b, err := kitutil.Marshal(v) if err != nil { return "{}" } diff --git a/service/relayconvert/internal/claude_messages/to_oai_chat_resp.go b/relaykit/relayconvert/internal/claude_messages/to_oai_chat_resp.go similarity index 96% rename from service/relayconvert/internal/claude_messages/to_oai_chat_resp.go rename to relaykit/relayconvert/internal/claude_messages/to_oai_chat_resp.go index 4f96eb79..55762335 100644 --- a/service/relayconvert/internal/claude_messages/to_oai_chat_resp.go +++ b/relaykit/relayconvert/internal/claude_messages/to_oai_chat_resp.go @@ -4,10 +4,10 @@ import ( "fmt" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" - "github.com/QuantumNous/new-api/relay/reasonmap" - sharedclaude "github.com/QuantumNous/new-api/service/relayconvert/internal/shared/claude" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/reasonmap" + sharedclaude "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/claude" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -50,7 +50,7 @@ func StreamResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.ChatCo } if claudeResponse.ContentBlock.Type == "tool_use" { tools = append(tools, dto.ToolCallResponse{ - Index: common.GetPointer(fcIdx), + Index: kitutil.GetPointer(fcIdx), ID: claudeResponse.ContentBlock.Id, Type: "function", Function: dto.FunctionResponse{ @@ -69,7 +69,7 @@ func StreamResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.ChatCo case "input_json_delta": tools = append(tools, dto.ToolCallResponse{ Type: "function", - Index: common.GetPointer(fcIdx), + Index: kitutil.GetPointer(fcIdx), Function: dto.FunctionResponse{ Arguments: *claudeResponse.Delta.PartialJson, }, @@ -105,9 +105,9 @@ func StreamResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.ChatCo func ResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.OpenAITextResponse { choices := make([]dto.OpenAITextResponseChoice, 0) fullTextResponse := dto.OpenAITextResponse{ - Id: fmt.Sprintf("chatcmpl-%s", common.GetUUID()), + Id: fmt.Sprintf("chatcmpl-%s", kitutil.GetUUID()), Object: "chat.completion", - Created: common.GetTimestamp(), + Created: kitutil.GetTimestamp(), } var responseText string var responseThinking string @@ -124,7 +124,7 @@ func ResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.OpenAITextRe for _, message := range claudeResponse.Content { switch message.Type { case "tool_use": - args, _ := common.Marshal(message.Input) + args, _ := kitutil.Marshal(message.Input) tools = append(tools, dto.ToolCallResponse{ ID: message.Id, Type: "function", diff --git a/service/relayconvert/internal/gemini_chat/to_oai_chat_req.go b/relaykit/relayconvert/internal/gemini_chat/to_oai_chat_req.go similarity index 81% rename from service/relayconvert/internal/gemini_chat/to_oai_chat_req.go rename to relaykit/relayconvert/internal/gemini_chat/to_oai_chat_req.go index 2557e801..b614bb8d 100644 --- a/service/relayconvert/internal/gemini_chat/to_oai_chat_req.go +++ b/relaykit/relayconvert/internal/gemini_chat/to_oai_chat_req.go @@ -4,23 +4,22 @@ import ( "fmt" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/service/relayconvert/internal/jsonutil" - relaymeta "github.com/QuantumNous/new-api/service/relayconvert/internal/meta" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/jsonutil" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" ) -func GeminiGenerateContentRequestToOpenAIChat(geminiRequest *dto.GeminiChatRequest, info *relaycommon.RelayInfo) (*dto.GeneralOpenAIRequest, error) { +func GeminiGenerateContentRequestToOpenAIChat(geminiRequest *dto.GeminiChatRequest, info convmeta.Meta) (*dto.GeneralOpenAIRequest, error) { modelName := "" isStream := false if info != nil { - isStream = info.IsStream + isStream = info.GetIsStream() } - modelName = relaymeta.RelayInfoUpstreamModelName(info) + modelName = convmeta.UpstreamModelName(info) openaiRequest := &dto.GeneralOpenAIRequest{ Model: modelName, - Stream: common.GetPointer(isStream), + Stream: kitutil.GetPointer(isStream), } var messages []dto.Message @@ -97,19 +96,19 @@ func GeminiGenerateContentRequestToOpenAIChat(geminiRequest *dto.GeminiChatReque openaiRequest.Temperature = geminiRequest.GenerationConfig.Temperature } if geminiRequest.GenerationConfig.TopP != nil && *geminiRequest.GenerationConfig.TopP > 0 { - openaiRequest.TopP = common.GetPointer(*geminiRequest.GenerationConfig.TopP) + openaiRequest.TopP = kitutil.GetPointer(*geminiRequest.GenerationConfig.TopP) } if geminiRequest.GenerationConfig.TopK != nil && *geminiRequest.GenerationConfig.TopK > 0 { - openaiRequest.TopK = common.GetPointer(int(*geminiRequest.GenerationConfig.TopK)) + openaiRequest.TopK = kitutil.GetPointer(int(*geminiRequest.GenerationConfig.TopK)) } if geminiRequest.GenerationConfig.MaxOutputTokens != nil && *geminiRequest.GenerationConfig.MaxOutputTokens > 0 { - openaiRequest.MaxTokens = common.GetPointer(*geminiRequest.GenerationConfig.MaxOutputTokens) + openaiRequest.MaxTokens = kitutil.GetPointer(*geminiRequest.GenerationConfig.MaxOutputTokens) } if len(geminiRequest.GenerationConfig.StopSequences) > 0 { openaiRequest.Stop = geminiRequest.GenerationConfig.StopSequences[:min(len(geminiRequest.GenerationConfig.StopSequences), 4)] } if geminiRequest.GenerationConfig.CandidateCount != nil && *geminiRequest.GenerationConfig.CandidateCount > 0 { - openaiRequest.N = common.GetPointer(*geminiRequest.GenerationConfig.CandidateCount) + openaiRequest.N = kitutil.GetPointer(*geminiRequest.GenerationConfig.CandidateCount) } if len(geminiRequest.GetTools()) > 0 { @@ -118,9 +117,9 @@ func GeminiGenerateContentRequestToOpenAIChat(geminiRequest *dto.GeminiChatReque if tool.FunctionDeclarations == nil { continue } - functionDeclarations, err := common.Any2Type[[]dto.FunctionRequest](tool.FunctionDeclarations) + functionDeclarations, err := kitutil.Any2Type[[]dto.FunctionRequest](tool.FunctionDeclarations) if err != nil { - common.SysError(fmt.Sprintf("failed to parse gemini function declarations: %v (type=%T)", err, tool.FunctionDeclarations)) + kitutil.LogSystemError(fmt.Sprintf("failed to parse gemini function declarations: %v (type=%T)", err, tool.FunctionDeclarations)) continue } for _, function := range functionDeclarations { diff --git a/service/relayconvert/internal/gemini_chat/to_oai_chat_resp.go b/relaykit/relayconvert/internal/gemini_chat/to_oai_chat_resp.go similarity index 90% rename from service/relayconvert/internal/gemini_chat/to_oai_chat_resp.go rename to relaykit/relayconvert/internal/gemini_chat/to_oai_chat_resp.go index 68181db3..e256fd10 100644 --- a/service/relayconvert/internal/gemini_chat/to_oai_chat_resp.go +++ b/relaykit/relayconvert/internal/gemini_chat/to_oai_chat_resp.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" + "github.com/QuantumNous/new-api/relaykit/types" ) func UsageFromGeminiMetadata(metadata *dto.GeminiUsageMetadata, fallbackPromptTokens int) *dto.Usage { @@ -91,7 +91,7 @@ func ResponseGeminiChat2OpenAI(id string, created int64, response *dto.GeminiCha Role: "assistant", Content: "", }, - FinishReason: constant.FinishReasonStop, + FinishReason: types.FinishReasonStop, } if len(candidate.Content.Parts) > 0 { var content strings.Builder @@ -130,7 +130,7 @@ func ResponseGeminiChat2OpenAI(id string, created int64, response *dto.GeminiCha content.WriteByte(')') } } else if part.FunctionCall != nil { - choice.FinishReason = constant.FinishReasonToolCalls + choice.FinishReason = types.FinishReasonToolCalls if call := geminiResponseToolCall(&part); call != nil { toolCalls = append(toolCalls, *call) } @@ -164,17 +164,17 @@ func ResponseGeminiChat2OpenAI(id string, created int64, response *dto.GeminiCha if candidate.FinishReason != nil { switch *candidate.FinishReason { case "STOP": - choice.FinishReason = constant.FinishReasonStop + choice.FinishReason = types.FinishReasonStop case "MAX_TOKENS": - choice.FinishReason = constant.FinishReasonLength + choice.FinishReason = types.FinishReasonLength case "SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII", "OTHER": - choice.FinishReason = constant.FinishReasonContentFilter + choice.FinishReason = types.FinishReasonContentFilter default: - choice.FinishReason = constant.FinishReasonContentFilter + choice.FinishReason = types.FinishReasonContentFilter } } if isToolCall { - choice.FinishReason = constant.FinishReasonToolCalls + choice.FinishReason = types.FinishReasonToolCalls } fullTextResponse.Choices = append(fullTextResponse.Choices, choice) @@ -216,13 +216,13 @@ func StreamResponseGeminiChat2OpenAI(geminiResponse *dto.GeminiChatResponse) (*d if candidate.FinishReason != nil { switch *candidate.FinishReason { case "STOP": - choice.FinishReason = &constant.FinishReasonStop + choice.FinishReason = &types.FinishReasonStop case "MAX_TOKENS": - choice.FinishReason = &constant.FinishReasonLength + choice.FinishReason = &types.FinishReasonLength case "SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII", "OTHER": - choice.FinishReason = &constant.FinishReasonContentFilter + choice.FinishReason = &types.FinishReasonContentFilter default: - choice.FinishReason = &constant.FinishReasonContentFilter + choice.FinishReason = &types.FinishReasonContentFilter } } for _, part := range candidate.Content.Parts { @@ -270,7 +270,7 @@ func StreamResponseGeminiChat2OpenAI(geminiResponse *dto.GeminiChatResponse) (*d choice.Delta.SetContentString(content.String()) } if isTools { - choice.FinishReason = &constant.FinishReasonToolCalls + choice.FinishReason = &types.FinishReasonToolCalls } choices = append(choices, choice) } @@ -283,12 +283,12 @@ func StreamResponseGeminiChat2OpenAI(geminiResponse *dto.GeminiChatResponse) (*d } func geminiResponseToolCall(item *dto.GeminiPart) *dto.ToolCallResponse { - argsBytes, err := common.Marshal(item.FunctionCall.Arguments) + argsBytes, err := kitutil.Marshal(item.FunctionCall.Arguments) if err != nil { return nil } return &dto.ToolCallResponse{ - ID: fmt.Sprintf("call_%s", common.GetUUID()), + ID: fmt.Sprintf("call_%s", kitutil.GetUUID()), Type: "function", Function: dto.FunctionResponse{ Arguments: string(argsBytes), diff --git a/service/relayconvert/internal/jsonutil/stringify.go b/relaykit/relayconvert/internal/jsonutil/stringify.go similarity index 59% rename from service/relayconvert/internal/jsonutil/stringify.go rename to relaykit/relayconvert/internal/jsonutil/stringify.go index 99ce8b89..a2565bbf 100644 --- a/service/relayconvert/internal/jsonutil/stringify.go +++ b/relaykit/relayconvert/internal/jsonutil/stringify.go @@ -3,11 +3,11 @@ package jsonutil import ( "fmt" - "github.com/QuantumNous/new-api/common" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" ) func ToJSONString(v interface{}) string { - bytes, err := common.Marshal(v) + bytes, err := kitutil.Marshal(v) if err != nil { return fmt.Sprintf("%v", v) } diff --git a/service/relayconvert/internal/media/media.go b/relaykit/relayconvert/internal/media/media.go similarity index 75% rename from service/relayconvert/internal/media/media.go rename to relaykit/relayconvert/internal/media/media.go index 3db158e4..50ea4336 100644 --- a/service/relayconvert/internal/media/media.go +++ b/relaykit/relayconvert/internal/media/media.go @@ -4,12 +4,12 @@ import ( "errors" "sync" - "github.com/QuantumNous/new-api/types" - "github.com/gin-gonic/gin" + "context" + "github.com/QuantumNous/new-api/relaykit/types" ) type MediaResolver struct { - GetBase64Data func(c *gin.Context, source types.FileSource, reason ...string) (string, string, error) + GetBase64Data func(c context.Context, source types.FileSource, reason ...string) (string, string, error) DecodeBase64FileData func(base64String string) (string, string, error) } @@ -25,7 +25,7 @@ func SetMediaResolver(resolver MediaResolver) { mediaResolver = resolver } -func ResolveBase64Data(c *gin.Context, source types.FileSource, reason ...string) (string, string, error) { +func ResolveBase64Data(c context.Context, source types.FileSource, reason ...string) (string, string, error) { mediaResolverMu.RLock() resolver := mediaResolver.GetBase64Data mediaResolverMu.RUnlock() diff --git a/service/relayconvert/internal/oai_chat/to_claude_messages_req.go b/relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go similarity index 81% rename from service/relayconvert/internal/oai_chat/to_claude_messages_req.go rename to relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go index 346eb906..54cb11d5 100644 --- a/service/relayconvert/internal/oai_chat/to_claude_messages_req.go +++ b/relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go @@ -5,13 +5,13 @@ import ( "fmt" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" - relaymedia "github.com/QuantumNous/new-api/service/relayconvert/internal/media" - sharedclaude "github.com/QuantumNous/new-api/service/relayconvert/internal/shared/claude" - "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/QuantumNous/new-api/setting/reasoning" - "github.com/gin-gonic/gin" + "context" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + relaymedia "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/media" + sharedclaude "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/claude" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" + "github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning" ) const ( @@ -27,7 +27,8 @@ type openRouterRequestReasoning struct { Exclude bool `json:"exclude,omitempty"` } -func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOpenAIRequest) (*dto.ClaudeRequest, error) { +func OpenAIChatRequestToClaudeMessages(c context.Context, info convmeta.Meta, textRequest dto.GeneralOpenAIRequest) (*dto.ClaudeRequest, error) { + opts := convmeta.OptionsOf(info) claudeTools := make([]any, 0, len(textRequest.Tools)) for _, tool := range textRequest.Tools { @@ -64,7 +65,7 @@ func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOp } var userLocationMap map[string]interface{} - if err := common.Unmarshal(textRequest.WebSearchOptions.UserLocation, &userLocationMap); err == nil { + if err := kitutil.Unmarshal(textRequest.WebSearchOptions.UserLocation, &userLocationMap); err == nil { if approximateData, ok := userLocationMap["approximate"].(map[string]interface{}); ok { if timezone, ok := approximateData["timezone"].(string); ok && timezone != "" { anthropicUserLocation.Timezone = timezone @@ -103,16 +104,16 @@ func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOp Tools: claudeTools, } if maxTokens := textRequest.GetMaxTokens(); maxTokens > 0 { - claudeRequest.MaxTokens = common.GetPointer(maxTokens) + claudeRequest.MaxTokens = kitutil.GetPointer(maxTokens) } if textRequest.TopP != nil { - claudeRequest.TopP = common.GetPointer(*textRequest.TopP) + claudeRequest.TopP = kitutil.GetPointer(*textRequest.TopP) } if textRequest.TopK != nil { - claudeRequest.TopK = common.GetPointer(*textRequest.TopK) + claudeRequest.TopK = kitutil.GetPointer(*textRequest.TopK) } if textRequest.IsStream(nil) { - claudeRequest.Stream = common.GetPointer(true) + claudeRequest.Stream = kitutil.GetPointer(true) } if textRequest.ToolChoice != nil || textRequest.ParallelTooCalls != nil { @@ -123,8 +124,10 @@ func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOp } if claudeRequest.MaxTokens == nil || *claudeRequest.MaxTokens == 0 { - defaultMaxTokens := uint(model_setting.GetClaudeSettings().GetDefaultMaxTokens(textRequest.Model)) - claudeRequest.MaxTokens = &defaultMaxTokens + if defaultMaxTokens, configured := opts.Claude.DefaultMaxTokensFor(textRequest.Model); configured { + value := uint(defaultMaxTokens) + claudeRequest.MaxTokens = &value + } } if baseModel, effortLevel, ok := reasoning.TrimEffortSuffix(textRequest.Model); ok && effortLevel != "" && @@ -144,9 +147,9 @@ func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOp claudeRequest.TopK = nil } else { claudeRequest.TopP = nil - claudeRequest.Temperature = common.GetPointer[float64](1.0) + claudeRequest.Temperature = kitutil.GetPointer[float64](1.0) } - } else if model_setting.GetClaudeSettings().ThinkingAdapterEnabled && + } else if opts.Claude.ThinkingAdapterEnabled && strings.HasSuffix(textRequest.Model, "-thinking") { trimmedModel := strings.TrimSuffix(textRequest.Model, "-thinking") @@ -159,17 +162,17 @@ func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOp claudeRequest.TopK = nil } else { if claudeRequest.MaxTokens == nil || *claudeRequest.MaxTokens < 1280 { - claudeRequest.MaxTokens = common.GetPointer[uint](1280) + claudeRequest.MaxTokens = kitutil.GetPointer[uint](1280) } claudeRequest.Thinking = &dto.Thinking{ Type: "enabled", - BudgetTokens: common.GetPointer[int](int(float64(*claudeRequest.MaxTokens) * model_setting.GetClaudeSettings().ThinkingAdapterBudgetTokensPercentage)), + BudgetTokens: kitutil.GetPointer[int](int(float64(*claudeRequest.MaxTokens) * opts.Claude.ThinkingAdapterBudgetTokensPercentage)), } claudeRequest.TopP = nil - claudeRequest.Temperature = common.GetPointer[float64](1.0) + claudeRequest.Temperature = kitutil.GetPointer[float64](1.0) } - if !model_setting.ShouldPreserveThinkingSuffix(textRequest.Model) { + if !opts.ShouldPreserveThinkingSuffix(textRequest.Model) { claudeRequest.Model = trimmedModel } } @@ -179,24 +182,24 @@ func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOp case "low": claudeRequest.Thinking = &dto.Thinking{ Type: "enabled", - BudgetTokens: common.GetPointer[int](1280), + BudgetTokens: kitutil.GetPointer[int](1280), } case "medium": claudeRequest.Thinking = &dto.Thinking{ Type: "enabled", - BudgetTokens: common.GetPointer[int](2048), + BudgetTokens: kitutil.GetPointer[int](2048), } case "high": claudeRequest.Thinking = &dto.Thinking{ Type: "enabled", - BudgetTokens: common.GetPointer[int](4096), + BudgetTokens: kitutil.GetPointer[int](4096), } } } if textRequest.Reasoning != nil { var reasoningConfig openRouterRequestReasoning - if err := common.Unmarshal(textRequest.Reasoning, &reasoningConfig); err != nil { + if err := kitutil.Unmarshal(textRequest.Reasoning, &reasoningConfig); err != nil { return nil, err } @@ -263,7 +266,7 @@ func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOp if text := message.StringContent(); text != "" { systemMessages = append(systemMessages, dto.ClaudeMediaMessage{ Type: "text", - Text: common.GetPointer[string](text), + Text: kitutil.GetPointer[string](text), }) } } else { @@ -271,7 +274,7 @@ func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOp if ctx.Type == "text" && ctx.Text != "" { systemMessages = append(systemMessages, dto.ClaudeMediaMessage{ Type: "text", - Text: common.GetPointer[string](ctx.Text), + Text: kitutil.GetPointer[string](ctx.Text), }) } } @@ -287,7 +290,7 @@ func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOp Content: []dto.ClaudeMediaMessage{ { Type: "text", - Text: common.GetPointer[string]("..."), + Text: kitutil.GetPointer[string]("..."), }, }, } @@ -305,7 +308,7 @@ func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOp lastClaudeMessage.Content = []dto.ClaudeMediaMessage{ { Type: "text", - Text: common.GetPointer[string](content), + Text: kitutil.GetPointer[string](content), }, } } @@ -340,7 +343,7 @@ func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOp if mediaMessage.Text != "" { claudeMediaMessages = append(claudeMediaMessages, dto.ClaudeMediaMessage{ Type: "text", - Text: common.GetPointer[string](mediaMessage.Text), + Text: kitutil.GetPointer[string](mediaMessage.Text), }) } default: @@ -374,8 +377,8 @@ func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOp for _, toolCall := range message.ParseToolCalls() { inputObj := make(map[string]any) if args := toolCall.Function.Arguments; args != "" { - if err := common.Unmarshal([]byte(args), &inputObj); err != nil { - common.SysLog("tool call function arguments is not a map[string]any: " + fmt.Sprintf("%v", toolCall.Function.Arguments)) + if err := kitutil.Unmarshal([]byte(args), &inputObj); err != nil { + kitutil.LogInfo("tool call function arguments is not a map[string]any: " + fmt.Sprintf("%v", toolCall.Function.Arguments)) } } claudeMediaMessages = append(claudeMediaMessages, dto.ClaudeMediaMessage{ @@ -397,5 +400,10 @@ func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOp claudeRequest.Prompt = "" claudeRequest.Messages = claudeMessages + // Checked last so every injection path (default hook, thinking adapter + // floor) has had its chance to satisfy the required field. + if claudeRequest.MaxTokens == nil { + return nil, sharedclaude.ErrMissingMaxTokens + } return &claudeRequest, nil } diff --git a/service/relayconvert/internal/oai_chat/to_claude_messages_resp.go b/relaykit/relayconvert/internal/oai_chat/to_claude_messages_resp.go similarity index 74% rename from service/relayconvert/internal/oai_chat/to_claude_messages_resp.go rename to relaykit/relayconvert/internal/oai_chat/to_claude_messages_resp.go index 7f0c6b8b..ff958b0b 100644 --- a/service/relayconvert/internal/oai_chat/to_claude_messages_resp.go +++ b/relaykit/relayconvert/internal/oai_chat/to_claude_messages_resp.go @@ -3,17 +3,17 @@ package oaichat import ( "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/relay/reasonmap" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/reasonmap" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" "github.com/samber/lo" ) func generateStopBlock(index int) *dto.ClaudeResponse { return &dto.ClaudeResponse{ Type: "content_block_stop", - Index: common.GetPointer[int](index), + Index: kitutil.GetPointer[int](index), } } @@ -72,16 +72,12 @@ func NormalizeCacheCreationSplit(totalTokens int, tokens5m int, tokens1h int) (i return tokens5m + remainder, tokens1h } -func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamResponse, info *relaycommon.RelayInfo) []*dto.ClaudeResponse { +func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamResponse, info convmeta.Meta) []*dto.ClaudeResponse { if info == nil { - info = &relaycommon.RelayInfo{} + info = &convmeta.Values{} } - if info.ClaudeConvertInfo == nil { - info.ClaudeConvertInfo = &relaycommon.ClaudeConvertInfo{ - LastMessagesType: relaycommon.LastMessageTypeNone, - } - } - if info.ClaudeConvertInfo.Done { + state := info.EnsureClaudeConvertInfo() + if state.Done { return nil } @@ -90,16 +86,16 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon // according to Anthropic's SSE streaming state machine: // content_block_start -> content_block_delta* -> content_block_stop (per index). // - // For text/thinking, there is at most one open block at info.ClaudeConvertInfo.Index. + // For text/thinking, there is at most one open block at state.Index. // For tools, OpenAI tool_calls can stream multiple parallel tool_use blocks (indexed from 0), // so we may have multiple open blocks and must stop each one explicitly. stopOpenBlocks := func() { - switch info.ClaudeConvertInfo.LastMessagesType { - case relaycommon.LastMessageTypeText, relaycommon.LastMessageTypeThinking: - claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index)) - case relaycommon.LastMessageTypeTools: - base := info.ClaudeConvertInfo.ToolCallBaseIndex - for offset := 0; offset <= info.ClaudeConvertInfo.ToolCallMaxIndexOffset; offset++ { + switch state.LastMessagesType { + case convmeta.LastMessageTypeText, convmeta.LastMessageTypeThinking: + claudeResponses = append(claudeResponses, generateStopBlock(state.Index)) + case convmeta.LastMessageTypeTools: + base := state.ToolCallBaseIndex + for offset := 0; offset <= state.ToolCallMaxIndexOffset; offset++ { claudeResponses = append(claudeResponses, generateStopBlock(base+offset)) } } @@ -110,21 +106,21 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon // This prevents invalid streams where a content_block_delta (e.g. thinking_delta) is emitted for an // index whose active content_block type is different (the typical cause of "Mismatched content block type"). stopOpenBlocksAndAdvance := func() { - if info.ClaudeConvertInfo.LastMessagesType == relaycommon.LastMessageTypeNone { + if state.LastMessagesType == convmeta.LastMessageTypeNone { return } stopOpenBlocks() - switch info.ClaudeConvertInfo.LastMessagesType { - case relaycommon.LastMessageTypeTools: - info.ClaudeConvertInfo.Index = info.ClaudeConvertInfo.ToolCallBaseIndex + info.ClaudeConvertInfo.ToolCallMaxIndexOffset + 1 - info.ClaudeConvertInfo.ToolCallBaseIndex = 0 - info.ClaudeConvertInfo.ToolCallMaxIndexOffset = 0 + switch state.LastMessagesType { + case convmeta.LastMessageTypeTools: + state.Index = state.ToolCallBaseIndex + state.ToolCallMaxIndexOffset + 1 + state.ToolCallBaseIndex = 0 + state.ToolCallMaxIndexOffset = 0 default: - info.ClaudeConvertInfo.Index++ + state.Index++ } - info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeNone + state.LastMessagesType = convmeta.LastMessageTypeNone } - if info.SendResponseCount == 1 { + if info.GetSendResponseCount() == 1 { msg := &dto.ClaudeMediaMessage{ Id: openAIResponse.Id, Model: openAIResponse.Model, @@ -144,9 +140,9 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon // Type: "ping", //}) if openAIResponse.IsToolCall() { - info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeTools - info.ClaudeConvertInfo.ToolCallBaseIndex = 0 - info.ClaudeConvertInfo.ToolCallMaxIndexOffset = 0 + state.LastMessagesType = convmeta.LastMessageTypeTools + state.ToolCallBaseIndex = 0 + state.ToolCallMaxIndexOffset = 0 var toolCall dto.ToolCallResponse if len(openAIResponse.Choices) > 0 && len(openAIResponse.Choices[0].Delta.ToolCalls) > 0 { toolCall = openAIResponse.Choices[0].Delta.ToolCalls[0] @@ -190,16 +186,16 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon content := openAIResponse.Choices[0].Delta.GetContentString() if reasoning != "" { - if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeThinking { + if state.LastMessagesType != convmeta.LastMessageTypeThinking { stopOpenBlocksAndAdvance() } - idx := info.ClaudeConvertInfo.Index + idx := state.Index claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ Index: &idx, Type: "content_block_start", ContentBlock: &dto.ClaudeMediaMessage{ Type: "thinking", - Thinking: common.GetPointer[string](""), + Thinking: kitutil.GetPointer[string](""), }, }) idx2 := idx @@ -211,18 +207,18 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon Thinking: &reasoning, }, }) - info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeThinking + state.LastMessagesType = convmeta.LastMessageTypeThinking } else if content != "" { - if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeText { + if state.LastMessagesType != convmeta.LastMessageTypeText { stopOpenBlocksAndAdvance() } - idx := info.ClaudeConvertInfo.Index + idx := state.Index claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ Index: &idx, Type: "content_block_start", ContentBlock: &dto.ClaudeMediaMessage{ Type: "text", - Text: common.GetPointer[string](""), + Text: kitutil.GetPointer[string](""), }, }) idx2 := idx @@ -231,34 +227,34 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon Type: "content_block_delta", Delta: &dto.ClaudeMediaMessage{ Type: "text_delta", - Text: common.GetPointer[string](content), + Text: kitutil.GetPointer[string](content), }, }) - info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeText + state.LastMessagesType = convmeta.LastMessageTypeText } } // 如果首块就带 finish_reason,需要立即发送停止块 if len(openAIResponse.Choices) > 0 && openAIResponse.Choices[0].FinishReason != nil && *openAIResponse.Choices[0].FinishReason != "" { - info.FinishReason = *openAIResponse.Choices[0].FinishReason + state.FinishReason = *openAIResponse.Choices[0].FinishReason stopOpenBlocks() oaiUsage := openAIResponse.Usage if oaiUsage == nil { - oaiUsage = info.ClaudeConvertInfo.Usage + oaiUsage = state.Usage } if oaiUsage != nil { claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ Type: "message_delta", Usage: buildClaudeUsageFromOpenAIUsage(oaiUsage), Delta: &dto.ClaudeMediaMessage{ - StopReason: common.GetPointer[string](stopReasonOpenAI2Claude(info.FinishReason)), + StopReason: kitutil.GetPointer[string](stopReasonOpenAI2Claude(state.FinishReason)), }, }) } claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ Type: "message_stop", }) - info.ClaudeConvertInfo.Done = true + state.Done = true } return claudeResponses } @@ -267,11 +263,11 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon // Some OpenAI-compatible upstreams end with a usage-only SSE chunk. oaiUsage := openAIResponse.Usage if oaiUsage == nil { - oaiUsage = info.ClaudeConvertInfo.Usage + oaiUsage = state.Usage } if oaiUsage != nil { stopOpenBlocks() - stopReason := stopReasonOpenAI2Claude(info.FinishReason) + stopReason := stopReasonOpenAI2Claude(state.FinishReason) if stopReason == "" { stopReason = "end_turn" } @@ -279,23 +275,23 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon Type: "message_delta", Usage: buildClaudeUsageFromOpenAIUsage(oaiUsage), Delta: &dto.ClaudeMediaMessage{ - StopReason: common.GetPointer[string](stopReason), + StopReason: kitutil.GetPointer[string](stopReason), }, }) claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ Type: "message_stop", }) - info.ClaudeConvertInfo.Done = true + state.Done = true } return claudeResponses } else { chosenChoice := openAIResponse.Choices[0] doneChunk := chosenChoice.FinishReason != nil && *chosenChoice.FinishReason != "" if doneChunk { - info.FinishReason = *chosenChoice.FinishReason + state.FinishReason = *chosenChoice.FinishReason oaiUsage := openAIResponse.Usage if oaiUsage == nil { - oaiUsage = info.ClaudeConvertInfo.Usage + oaiUsage = state.Usage // Some upstreams emit finish_reason first, then send a final usage-only chunk. // Defer closing until usage is available so the final message_delta carries it. return claudeResponses @@ -307,14 +303,14 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon claudeResponse.Type = "content_block_delta" if len(chosenChoice.Delta.ToolCalls) > 0 { toolCalls := chosenChoice.Delta.ToolCalls - if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeTools { + if state.LastMessagesType != convmeta.LastMessageTypeTools { stopOpenBlocksAndAdvance() - info.ClaudeConvertInfo.ToolCallBaseIndex = info.ClaudeConvertInfo.Index - info.ClaudeConvertInfo.ToolCallMaxIndexOffset = 0 + state.ToolCallBaseIndex = state.Index + state.ToolCallMaxIndexOffset = 0 } - info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeTools - base := info.ClaudeConvertInfo.ToolCallBaseIndex - maxOffset := info.ClaudeConvertInfo.ToolCallMaxIndexOffset + state.LastMessagesType = convmeta.LastMessageTypeTools + base := state.ToolCallBaseIndex + maxOffset := state.ToolCallMaxIndexOffset for i, toolCall := range toolCalls { offset := 0 @@ -353,47 +349,47 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon }) } } - info.ClaudeConvertInfo.ToolCallMaxIndexOffset = maxOffset - info.ClaudeConvertInfo.Index = base + maxOffset + state.ToolCallMaxIndexOffset = maxOffset + state.Index = base + maxOffset } else { reasoning := chosenChoice.Delta.GetReasoningContent() textContent := chosenChoice.Delta.GetContentString() if reasoning != "" || textContent != "" { if reasoning != "" { - if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeThinking { + if state.LastMessagesType != convmeta.LastMessageTypeThinking { stopOpenBlocksAndAdvance() - idx := info.ClaudeConvertInfo.Index + idx := state.Index claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ Index: &idx, Type: "content_block_start", ContentBlock: &dto.ClaudeMediaMessage{ Type: "thinking", - Thinking: common.GetPointer[string](""), + Thinking: kitutil.GetPointer[string](""), }, }) } - info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeThinking + state.LastMessagesType = convmeta.LastMessageTypeThinking claudeResponse.Delta = &dto.ClaudeMediaMessage{ Type: "thinking_delta", Thinking: &reasoning, } } else { - if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeText { + if state.LastMessagesType != convmeta.LastMessageTypeText { stopOpenBlocksAndAdvance() - idx := info.ClaudeConvertInfo.Index + idx := state.Index claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ Index: &idx, Type: "content_block_start", ContentBlock: &dto.ClaudeMediaMessage{ Type: "text", - Text: common.GetPointer[string](""), + Text: kitutil.GetPointer[string](""), }, }) } - info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeText + state.LastMessagesType = convmeta.LastMessageTypeText claudeResponse.Delta = &dto.ClaudeMediaMessage{ Type: "text_delta", - Text: common.GetPointer[string](textContent), + Text: kitutil.GetPointer[string](textContent), } } } else { @@ -401,30 +397,30 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon } } - claudeResponse.Index = common.GetPointer[int](info.ClaudeConvertInfo.Index) + claudeResponse.Index = kitutil.GetPointer[int](state.Index) if !isEmpty && claudeResponse.Delta != nil { claudeResponses = append(claudeResponses, &claudeResponse) } - if doneChunk || info.ClaudeConvertInfo.Done { + if doneChunk || state.Done { stopOpenBlocks() oaiUsage := openAIResponse.Usage if oaiUsage == nil { - oaiUsage = info.ClaudeConvertInfo.Usage + oaiUsage = state.Usage } if oaiUsage != nil { claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ Type: "message_delta", Usage: buildClaudeUsageFromOpenAIUsage(oaiUsage), Delta: &dto.ClaudeMediaMessage{ - StopReason: common.GetPointer[string](stopReasonOpenAI2Claude(info.FinishReason)), + StopReason: kitutil.GetPointer[string](stopReasonOpenAI2Claude(state.FinishReason)), }, }) } claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ Type: "message_stop", }) - info.ClaudeConvertInfo.Done = true + state.Done = true return claudeResponses } } @@ -432,7 +428,7 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon return claudeResponses } -func ResponseOpenAI2Claude(openAIResponse *dto.OpenAITextResponse, info *relaycommon.RelayInfo) *dto.ClaudeResponse { +func ResponseOpenAI2Claude(openAIResponse *dto.OpenAITextResponse, info convmeta.Meta) *dto.ClaudeResponse { var stopReason string contents := make([]dto.ClaudeMediaMessage, 0) claudeResponse := &dto.ClaudeResponse{ @@ -459,7 +455,7 @@ func ResponseOpenAI2Claude(openAIResponse *dto.OpenAITextResponse, info *relayco mapParams := map[string]interface{}{} if strings.TrimSpace(toolUse.Function.Arguments) != "" { var parsed map[string]interface{} - if err := common.Unmarshal([]byte(toolUse.Function.Arguments), &parsed); err == nil && parsed != nil { + if err := kitutil.Unmarshal([]byte(toolUse.Function.Arguments), &parsed); err == nil && parsed != nil { mapParams = parsed } } diff --git a/service/relayconvert/internal/oai_chat/to_claude_messages_resp_test.go b/relaykit/relayconvert/internal/oai_chat/to_claude_messages_resp_test.go similarity index 96% rename from service/relayconvert/internal/oai_chat/to_claude_messages_resp_test.go rename to relaykit/relayconvert/internal/oai_chat/to_claude_messages_resp_test.go index facd873e..1ccdd00b 100644 --- a/service/relayconvert/internal/oai_chat/to_claude_messages_resp_test.go +++ b/relaykit/relayconvert/internal/oai_chat/to_claude_messages_resp_test.go @@ -3,8 +3,8 @@ package oaichat import ( "testing" - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -104,9 +104,9 @@ func TestBuildClaudeUsageFromOpenAICacheWriteUsage(t *testing.T) { } func TestStreamResponseOpenAI2ClaudeClosesTextThinkingAndToolBlocks(t *testing.T) { - info := &relaycommon.RelayInfo{ - ClaudeConvertInfo: &relaycommon.ClaudeConvertInfo{ - LastMessagesType: relaycommon.LastMessageTypeNone, + info := &convmeta.Values{ + ClaudeConvertInfo: &convmeta.ClaudeConvertInfo{ + LastMessagesType: convmeta.LastMessageTypeNone, }, } diff --git a/service/relayconvert/internal/oai_chat/to_gemini_chat_req.go b/relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req.go similarity index 86% rename from service/relayconvert/internal/oai_chat/to_gemini_chat_req.go rename to relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req.go index 78622182..2ebb9348 100644 --- a/service/relayconvert/internal/oai_chat/to_gemini_chat_req.go +++ b/relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req.go @@ -5,17 +5,16 @@ import ( "fmt" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" - relaymedia "github.com/QuantumNous/new-api/service/relayconvert/internal/media" - relaymeta "github.com/QuantumNous/new-api/service/relayconvert/internal/meta" - sharedgemini "github.com/QuantumNous/new-api/service/relayconvert/internal/shared/gemini" - "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/gin-gonic/gin" + "context" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + relaymedia "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/media" + sharedgemini "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/gemini" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" ) -func OpenAIChatRequestToGeminiGenerateContent(c *gin.Context, textRequest dto.GeneralOpenAIRequest, info *relaycommon.RelayInfo) (*dto.GeminiChatRequest, error) { +func OpenAIChatRequestToGeminiGenerateContent(c context.Context, textRequest dto.GeneralOpenAIRequest, info convmeta.Meta) (*dto.GeminiChatRequest, error) { + opts := convmeta.OptionsOf(info) geminiRequest := dto.GeminiChatRequest{ Contents: make([]dto.GeminiChatContent, 0, len(textRequest.Messages)), GenerationConfig: dto.GeminiChatGenerationConfig{ @@ -24,21 +23,21 @@ func OpenAIChatRequestToGeminiGenerateContent(c *gin.Context, textRequest dto.Ge } if textRequest.TopP != nil && *textRequest.TopP > 0 { - geminiRequest.GenerationConfig.TopP = common.GetPointer(*textRequest.TopP) + geminiRequest.GenerationConfig.TopP = kitutil.GetPointer(*textRequest.TopP) } if maxTokens := textRequest.GetMaxTokens(); maxTokens > 0 { - geminiRequest.GenerationConfig.MaxOutputTokens = common.GetPointer(maxTokens) + geminiRequest.GenerationConfig.MaxOutputTokens = kitutil.GetPointer(maxTokens) } if textRequest.Seed != nil && *textRequest.Seed != 0 { - geminiRequest.GenerationConfig.Seed = common.GetPointer(int64(*textRequest.Seed)) + geminiRequest.GenerationConfig.Seed = kitutil.GetPointer(int64(*textRequest.Seed)) } upstreamModelName := textRequest.Model - if modelName := relaymeta.RelayInfoUpstreamModelName(info); modelName != "" { + if modelName := convmeta.UpstreamModelName(info); modelName != "" { upstreamModelName = modelName } - if model_setting.IsGeminiModelSupportImagine(upstreamModelName) { + if opts.Gemini.SupportsImagineModel(upstreamModelName) { geminiRequest.GenerationConfig.ResponseModalities = []string{ "TEXT", "IMAGE", @@ -54,7 +53,7 @@ func OpenAIChatRequestToGeminiGenerateContent(c *gin.Context, textRequest dto.Ge adaptorWithExtraBody := false if len(textRequest.ExtraBody) > 0 { var extraBody map[string]interface{} - if err := common.Unmarshal(textRequest.ExtraBody, &extraBody); err != nil { + if err := kitutil.Unmarshal(textRequest.ExtraBody, &extraBody); err != nil { return nil, fmt.Errorf("invalid extra body: %w", err) } @@ -76,7 +75,7 @@ func OpenAIChatRequestToGeminiGenerateContent(c *gin.Context, textRequest dto.Ge switch v := thinkingBudget.(type) { case float64: budgetInt := int(v) - tempThinkingConfig.ThinkingBudget = common.GetPointer(budgetInt) + tempThinkingConfig.ThinkingBudget = kitutil.GetPointer(budgetInt) tempThinkingConfig.IncludeThoughts = budgetInt > 0 hasThinkingConfig = true default: @@ -138,7 +137,7 @@ func OpenAIChatRequestToGeminiGenerateContent(c *gin.Context, textRequest dto.Ge } if len(geminiImageConfig) > 0 { - imageConfigBytes, err := common.Marshal(geminiImageConfig) + imageConfigBytes, err := kitutil.Marshal(geminiImageConfig) if err != nil { return nil, fmt.Errorf("failed to marshal image_config: %w", err) } @@ -152,14 +151,20 @@ func OpenAIChatRequestToGeminiGenerateContent(c *gin.Context, textRequest dto.Ge sharedgemini.ApplyThinkingConfig(&geminiRequest, info, textRequest) } - safetySettings := make([]dto.GeminiChatSafetySettings, 0, len(sharedgemini.SafetySettingCategories)) + var safetySettings []dto.GeminiChatSafetySettings for _, category := range sharedgemini.SafetySettingCategories { + threshold := opts.Gemini.SafetySettingFor(category) + if threshold == "" { + continue + } safetySettings = append(safetySettings, dto.GeminiChatSafetySettings{ Category: category, - Threshold: model_setting.GetGeminiSafetySetting(category), + Threshold: threshold, }) } - geminiRequest.SafetySettings = safetySettings + if len(safetySettings) > 0 { + geminiRequest.SafetySettings = safetySettings + } if textRequest.Tools != nil { functions := make([]dto.FunctionRequest, 0, len(textRequest.Tools)) @@ -222,7 +227,7 @@ func OpenAIChatRequestToGeminiGenerateContent(c *gin.Context, textRequest dto.Ge if len(textRequest.ResponseFormat.JsonSchema) > 0 { var jsonSchema dto.FormatJsonSchema - if err := common.Unmarshal(textRequest.ResponseFormat.JsonSchema, &jsonSchema); err == nil { + if err := kitutil.Unmarshal(textRequest.ResponseFormat.JsonSchema, &jsonSchema); err == nil { cleanedSchema := sharedgemini.RemoveAdditionalProperties(jsonSchema.Schema, 0) geminiRequest.GenerationConfig.ResponseSchema = cleanedSchema } @@ -252,9 +257,9 @@ func OpenAIChatRequestToGeminiGenerateContent(c *gin.Context, textRequest dto.Ge var contentMap map[string]interface{} contentStr := message.StringContent() - if err := common.Unmarshal([]byte(contentStr), &contentMap); err != nil { + if err := kitutil.Unmarshal([]byte(contentStr), &contentMap); err != nil { var contentSlice []interface{} - if err := common.Unmarshal([]byte(contentStr), &contentSlice); err == nil { + if err := kitutil.Unmarshal([]byte(contentStr), &contentSlice); err == nil { contentMap = map[string]interface{}{"result": contentSlice} } else { contentMap = map[string]interface{}{"content": contentStr} @@ -276,13 +281,13 @@ func OpenAIChatRequestToGeminiGenerateContent(c *gin.Context, textRequest dto.Ge content := dto.GeminiChatContent{ Role: message.Role, } - shouldAttachThoughtSignature := (message.Role == "assistant" || message.Role == "model") && sharedgemini.ShouldAttachThoughtSignature() + shouldAttachThoughtSignature := (message.Role == "assistant" || message.Role == "model") && sharedgemini.ShouldAttachThoughtSignature(opts) signatureAttached := false if message.ToolCalls != nil { for _, call := range message.ParseToolCalls() { args := map[string]interface{}{} if call.Function.Arguments != "" { - if common.Unmarshal([]byte(call.Function.Arguments), &args) != nil { + if kitutil.Unmarshal([]byte(call.Function.Arguments), &args) != nil { return nil, fmt.Errorf("invalid arguments for function %s, args: %s", call.Function.Name, call.Function.Arguments) } } @@ -292,7 +297,7 @@ func OpenAIChatRequestToGeminiGenerateContent(c *gin.Context, textRequest dto.Ge Arguments: args, }, } - if shouldAttachThoughtSignature && !signatureAttached && sharedgemini.AttachFunctionCallThoughtSignature(&toolCall) { + if shouldAttachThoughtSignature && !signatureAttached && sharedgemini.AttachFunctionCallThoughtSignature(opts, &toolCall) { signatureAttached = true } parts = append(parts, toolCall) @@ -346,7 +351,7 @@ func OpenAIChatRequestToGeminiGenerateContent(c *gin.Context, textRequest dto.Ge }, } if shouldAttachThoughtSignature { - sharedgemini.AttachThoughtSignatureBypass(&imgPart) + sharedgemini.AttachThoughtSignatureBypass(opts, &imgPart) } parts = append(parts, imgPart) text = text[closeIdx+1:] @@ -380,7 +385,7 @@ func OpenAIChatRequestToGeminiGenerateContent(c *gin.Context, textRequest dto.Ge } if shouldAttachThoughtSignature && !signatureAttached && len(parts) > 0 { - sharedgemini.AttachFirstTextThoughtSignature(parts) + sharedgemini.AttachFirstTextThoughtSignature(opts, parts) } content.Parts = parts diff --git a/service/relayconvert/internal/oai_chat/to_gemini_chat_resp.go b/relaykit/relayconvert/internal/oai_chat/to_gemini_chat_resp.go similarity index 93% rename from service/relayconvert/internal/oai_chat/to_gemini_chat_resp.go rename to relaykit/relayconvert/internal/oai_chat/to_gemini_chat_resp.go index fb4df295..9c54d3c0 100644 --- a/service/relayconvert/internal/oai_chat/to_gemini_chat_resp.go +++ b/relaykit/relayconvert/internal/oai_chat/to_gemini_chat_resp.go @@ -1,13 +1,13 @@ package oaichat import ( - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" ) // ResponseOpenAI2Gemini 将 OpenAI 响应转换为 Gemini 格式 -func ResponseOpenAI2Gemini(openAIResponse *dto.OpenAITextResponse, info *relaycommon.RelayInfo) *dto.GeminiChatResponse { +func ResponseOpenAI2Gemini(openAIResponse *dto.OpenAITextResponse, info convmeta.Meta) *dto.GeminiChatResponse { totalTokens := openAIResponse.TotalTokens if totalTokens == 0 { totalTokens = openAIResponse.PromptTokens + openAIResponse.CompletionTokens @@ -66,7 +66,7 @@ func ResponseOpenAI2Gemini(openAIResponse *dto.OpenAITextResponse, info *relayco for _, toolCall := range toolCalls { var args map[string]interface{} if toolCall.Function.Arguments != "" { - if err := common.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil { + if err := kitutil.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil { args = map[string]interface{}{"arguments": toolCall.Function.Arguments} } } else { @@ -90,7 +90,7 @@ func ResponseOpenAI2Gemini(openAIResponse *dto.OpenAITextResponse, info *relayco } // StreamResponseOpenAI2Gemini 将 OpenAI 流式响应转换为 Gemini 格式 -func StreamResponseOpenAI2Gemini(openAIResponse *dto.ChatCompletionsStreamResponse, info *relaycommon.RelayInfo) *dto.GeminiChatResponse { +func StreamResponseOpenAI2Gemini(openAIResponse *dto.ChatCompletionsStreamResponse, info convmeta.Meta) *dto.GeminiChatResponse { // 检查是否有实际内容或结束标志 hasContent := false hasFinishReason := false @@ -168,7 +168,7 @@ func StreamResponseOpenAI2Gemini(openAIResponse *dto.ChatCompletionsStreamRespon // 解析参数 var args map[string]interface{} if toolCall.Function.Arguments != "" { - if err := common.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil { + if err := kitutil.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil { args = map[string]interface{}{"arguments": toolCall.Function.Arguments} } } else { diff --git a/service/relayconvert/internal/oai_chat/to_gemini_chat_resp_test.go b/relaykit/relayconvert/internal/oai_chat/to_gemini_chat_resp_test.go similarity index 96% rename from service/relayconvert/internal/oai_chat/to_gemini_chat_resp_test.go rename to relaykit/relayconvert/internal/oai_chat/to_gemini_chat_resp_test.go index 669d10a2..4e20baf4 100644 --- a/service/relayconvert/internal/oai_chat/to_gemini_chat_resp_test.go +++ b/relaykit/relayconvert/internal/oai_chat/to_gemini_chat_resp_test.go @@ -3,8 +3,8 @@ package oaichat import ( "testing" - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -87,7 +87,7 @@ func TestStreamResponseOpenAI2GeminiMapsToolCallFinishReasonAndUsage(t *testing. CompletionTokens: 8, TotalTokens: 21, }, - }, &relaycommon.RelayInfo{}) + }, &convmeta.Values{}) require.NotNil(t, resp) assert.Equal(t, 13, resp.UsageMetadata.PromptTokenCount) diff --git a/service/relayconvert/internal/oai_chat/to_oai_responses_req.go b/relaykit/relayconvert/internal/oai_chat/to_oai_responses_req.go similarity index 88% rename from service/relayconvert/internal/oai_chat/to_oai_responses_req.go rename to relaykit/relayconvert/internal/oai_chat/to_oai_responses_req.go index 0da9f449..b4acb511 100644 --- a/service/relayconvert/internal/oai_chat/to_oai_responses_req.go +++ b/relaykit/relayconvert/internal/oai_chat/to_oai_responses_req.go @@ -6,8 +6,8 @@ import ( "fmt" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" "github.com/samber/lo" ) @@ -16,7 +16,7 @@ func normalizeChatImageURLToString(v any) any { case string: return vv case map[string]any: - if url := common.Interface2String(vv["url"]); url != "" { + if url := kitutil.Interface2String(vv["url"]); url != "" { return url } return v @@ -46,7 +46,7 @@ func convertChatResponseFormatToResponsesText(reqFormat *dto.ResponseFormat) jso if reqFormat.Type == "json_schema" && len(reqFormat.JsonSchema) > 0 { var chatSchema map[string]any - if err := common.Unmarshal(reqFormat.JsonSchema, &chatSchema); err == nil { + if err := kitutil.Unmarshal(reqFormat.JsonSchema, &chatSchema); err == nil { for key, value := range chatSchema { if key == "type" { continue @@ -67,7 +67,7 @@ func convertChatResponseFormatToResponsesText(reqFormat *dto.ResponseFormat) jso } } - textRaw, _ := common.Marshal(map[string]any{ + textRaw, _ := kitutil.Marshal(map[string]any{ "format": format, }) return textRaw @@ -102,7 +102,7 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d } else if msg.IsStringContent() { output = msg.StringContent() } else { - if b, err := common.Marshal(msg.Content); err == nil { + if b, err := kitutil.Marshal(msg.Content); err == nil { output = string(b) } else { output = fmt.Sprintf("%v", msg.Content) @@ -274,7 +274,7 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d } } - inputRaw, err := common.Marshal(inputItems) + inputRaw, err := kitutil.Marshal(inputItems) if err != nil { return nil, err } @@ -282,7 +282,7 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d var instructionsRaw json.RawMessage if len(instructionsParts) > 0 { instructions := strings.Join(instructionsParts, "\n\n") - instructionsRaw, _ = common.Marshal(instructions) + instructionsRaw, _ = kitutil.Marshal(instructions) } var toolsRaw json.RawMessage @@ -300,8 +300,8 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d default: // Best-effort: keep original tool shape for unknown types. var m map[string]any - if b, err := common.Marshal(tool); err == nil { - _ = common.Unmarshal(b, &m) + if b, err := kitutil.Marshal(tool); err == nil { + _ = kitutil.Unmarshal(b, &m) } if len(m) == 0 { m = map[string]any{"type": tool.Type} @@ -309,50 +309,50 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d tools = append(tools, m) } } - toolsRaw, _ = common.Marshal(tools) + toolsRaw, _ = kitutil.Marshal(tools) } var toolChoiceRaw json.RawMessage if req.ToolChoice != nil { switch v := req.ToolChoice.(type) { case string: - toolChoiceRaw, _ = common.Marshal(v) + toolChoiceRaw, _ = kitutil.Marshal(v) default: var m map[string]any - if b, err := common.Marshal(v); err == nil { - _ = common.Unmarshal(b, &m) + if b, err := kitutil.Marshal(v); err == nil { + _ = kitutil.Unmarshal(b, &m) } if m == nil { - toolChoiceRaw, _ = common.Marshal(v) + toolChoiceRaw, _ = kitutil.Marshal(v) } else if t, _ := m["type"].(string); t == "function" { // Chat: {"type":"function","function":{"name":"..."}} // Responses: {"type":"function","name":"..."} if name, ok := m["name"].(string); ok && name != "" { - toolChoiceRaw, _ = common.Marshal(map[string]any{ + toolChoiceRaw, _ = kitutil.Marshal(map[string]any{ "type": "function", "name": name, }) } else if fn, ok := m["function"].(map[string]any); ok { if name, ok := fn["name"].(string); ok && name != "" { - toolChoiceRaw, _ = common.Marshal(map[string]any{ + toolChoiceRaw, _ = kitutil.Marshal(map[string]any{ "type": "function", "name": name, }) } else { - toolChoiceRaw, _ = common.Marshal(v) + toolChoiceRaw, _ = kitutil.Marshal(v) } } else { - toolChoiceRaw, _ = common.Marshal(v) + toolChoiceRaw, _ = kitutil.Marshal(v) } } else { - toolChoiceRaw, _ = common.Marshal(v) + toolChoiceRaw, _ = kitutil.Marshal(v) } } } var parallelToolCallsRaw json.RawMessage if req.ParallelTooCalls != nil { - parallelToolCallsRaw, _ = common.Marshal(*req.ParallelTooCalls) + parallelToolCallsRaw, _ = kitutil.Marshal(*req.ParallelTooCalls) } textRaw := convertChatResponseFormatToResponsesText(req.ResponseFormat) @@ -369,7 +369,7 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d var topP *float64 if req.TopP != nil { - topP = common.GetPointer(lo.FromPtr(req.TopP)) + topP = kitutil.GetPointer(lo.FromPtr(req.TopP)) } out := &dto.OpenAIResponsesRequest{ diff --git a/service/relayconvert/internal/oai_chat/to_oai_responses_req_test.go b/relaykit/relayconvert/internal/oai_chat/to_oai_responses_req_test.go similarity index 97% rename from service/relayconvert/internal/oai_chat/to_oai_responses_req_test.go rename to relaykit/relayconvert/internal/oai_chat/to_oai_responses_req_test.go index 1637603a..095a540d 100644 --- a/service/relayconvert/internal/oai_chat/to_oai_responses_req_test.go +++ b/relaykit/relayconvert/internal/oai_chat/to_oai_responses_req_test.go @@ -3,7 +3,7 @@ package oaichat import ( "testing" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/service/relayconvert/internal/oai_chat/to_oai_responses_resp.go b/relaykit/relayconvert/internal/oai_chat/to_oai_responses_resp.go similarity index 96% rename from service/relayconvert/internal/oai_chat/to_oai_responses_resp.go rename to relaykit/relayconvert/internal/oai_chat/to_oai_responses_resp.go index b3a15ad9..e732819d 100644 --- a/service/relayconvert/internal/oai_chat/to_oai_responses_resp.go +++ b/relaykit/relayconvert/internal/oai_chat/to_oai_responses_resp.go @@ -6,8 +6,8 @@ import ( "strings" "time" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" ) const ( @@ -165,7 +165,7 @@ func responseStatusString(resp *dto.OpenAIResponsesResponse) string { return "" } var status string - _ = common.Unmarshal(resp.Status, &status) + _ = kitutil.Unmarshal(resp.Status, &status) return strings.TrimSpace(status) } @@ -194,7 +194,7 @@ func chatToolCallToResponsesOutput(toolCall dto.ToolCallRequest, responseID stri } func chatArgumentsRawMessage(arguments string) []byte { - raw, err := common.Marshal(arguments) + raw, err := kitutil.Marshal(arguments) if err != nil { return []byte(`""`) } @@ -212,7 +212,7 @@ func chatCreatedAt(created any) int { case float32: return int(v) case string: - if parsed := common.String2Int(v); parsed != 0 { + if parsed := kitutil.String2Int(v); parsed != 0 { return parsed } } diff --git a/service/relayconvert/internal/oai_chat/to_oai_responses_resp_test.go b/relaykit/relayconvert/internal/oai_chat/to_oai_responses_resp_test.go similarity index 99% rename from service/relayconvert/internal/oai_chat/to_oai_responses_resp_test.go rename to relaykit/relayconvert/internal/oai_chat/to_oai_responses_resp_test.go index a5034a0e..a6d752c9 100644 --- a/service/relayconvert/internal/oai_chat/to_oai_responses_resp_test.go +++ b/relaykit/relayconvert/internal/oai_chat/to_oai_responses_resp_test.go @@ -3,7 +3,7 @@ package oaichat import ( "testing" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/service/relayconvert/internal/oai_chat/to_oai_responses_stream_resp.go b/relaykit/relayconvert/internal/oai_chat/to_oai_responses_stream_resp.go similarity index 99% rename from service/relayconvert/internal/oai_chat/to_oai_responses_stream_resp.go rename to relaykit/relayconvert/internal/oai_chat/to_oai_responses_stream_resp.go index 75c602dd..301afc76 100644 --- a/service/relayconvert/internal/oai_chat/to_oai_responses_stream_resp.go +++ b/relaykit/relayconvert/internal/oai_chat/to_oai_responses_stream_resp.go @@ -6,7 +6,7 @@ import ( "strings" "time" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" ) type ChatToResponsesStreamEvent struct { diff --git a/service/relayconvert/internal/oai_responses/req_helpers.go b/relaykit/relayconvert/internal/oai_responses/req_helpers.go similarity index 79% rename from service/relayconvert/internal/oai_responses/req_helpers.go rename to relaykit/relayconvert/internal/oai_responses/req_helpers.go index 969fb1ec..15544b2c 100644 --- a/service/relayconvert/internal/oai_responses/req_helpers.go +++ b/relaykit/relayconvert/internal/oai_responses/req_helpers.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" + "github.com/QuantumNous/new-api/relaykit/types" ) func openAIResponsesRequestFromAny(request any) (*dto.OpenAIResponsesRequest, error) { @@ -31,7 +31,7 @@ func responsesInputItems(raw []byte) ([]map[string]any, error) { return nil, nil } - switch common.GetJsonType(raw) { + switch kitutil.GetJsonType(raw) { case "string": input, err := responsesJSONString(raw) if err != nil { @@ -45,12 +45,12 @@ func responsesInputItems(raw []byte) ([]map[string]any, error) { }, nil case "array": var items []map[string]any - if err := common.Unmarshal(raw, &items); err != nil { + if err := kitutil.Unmarshal(raw, &items); err != nil { return nil, fmt.Errorf("invalid input array: %w", err) } return items, nil default: - return nil, fmt.Errorf("unsupported responses input type %q", common.GetJsonType(raw)) + return nil, fmt.Errorf("unsupported responses input type %q", kitutil.GetJsonType(raw)) } } @@ -75,7 +75,7 @@ func responsesContentParts(content any) ([]map[string]any, error) { case map[string]any: parts = append(parts, part) default: - raw, err := common.Marshal(part) + raw, err := kitutil.Marshal(part) if err != nil { return nil, err } @@ -84,7 +84,7 @@ func responsesContentParts(content any) ([]map[string]any, error) { } return parts, nil default: - raw, err := common.Marshal(typed) + raw, err := kitutil.Marshal(typed) if err != nil { return nil, err } @@ -102,22 +102,22 @@ func responsesRequestFunctionDeclarations(raw []byte) ([]dto.FunctionRequest, er } var tools []map[string]any - if err := common.Unmarshal(raw, &tools); err != nil { + if err := kitutil.Unmarshal(raw, &tools); err != nil { return nil, fmt.Errorf("invalid tools: %w", err) } functions := make([]dto.FunctionRequest, 0, len(tools)) for _, tool := range tools { - if strings.TrimSpace(common.Interface2String(tool["type"])) != "function" { + if strings.TrimSpace(kitutil.Interface2String(tool["type"])) != "function" { continue } - name := strings.TrimSpace(common.Interface2String(tool["name"])) + name := strings.TrimSpace(kitutil.Interface2String(tool["name"])) if name == "" { continue } functions = append(functions, dto.FunctionRequest{ Name: name, - Description: common.Interface2String(tool["description"]), + Description: kitutil.Interface2String(tool["description"]), Parameters: tool["parameters"], }) } @@ -147,11 +147,11 @@ func responsesObjectValue(value any, fallbackKey string) map[string]any { return typed case string: var object map[string]any - if err := common.Unmarshal([]byte(typed), &object); err == nil { + if err := kitutil.Unmarshal([]byte(typed), &object); err == nil { return object } var array []any - if err := common.Unmarshal([]byte(typed), &array); err == nil { + if err := kitutil.Unmarshal([]byte(typed), &array); err == nil { return map[string]any{fallbackKey: array} } return map[string]any{fallbackKey: typed} @@ -174,11 +174,11 @@ func responsesGeminiResponseMap(value any) map[string]interface{} { return typed case string: var object map[string]interface{} - if err := common.Unmarshal([]byte(typed), &object); err == nil { + if err := kitutil.Unmarshal([]byte(typed), &object); err == nil { return object } var array []interface{} - if err := common.Unmarshal([]byte(typed), &array); err == nil { + if err := kitutil.Unmarshal([]byte(typed), &array); err == nil { return map[string]interface{}{"result": array} } return map[string]interface{}{"content": typed} @@ -194,11 +194,11 @@ func GeminiResponseMap(value any) map[string]interface{} { } func responsesParallelToolCalls(raw []byte) *bool { - if !rawJSONPresent(raw) || common.GetJsonType(raw) != "boolean" { + if !rawJSONPresent(raw) || kitutil.GetJsonType(raw) != "boolean" { return nil } var parallelToolCalls bool - if err := common.Unmarshal(raw, ¶llelToolCalls); err != nil { + if err := kitutil.Unmarshal(raw, ¶llelToolCalls); err != nil { return nil } return ¶llelToolCalls @@ -209,7 +209,7 @@ func ParallelToolCalls(raw []byte) *bool { } func ContentPartToFileSource(part map[string]any) types.FileSource { - partType := strings.TrimSpace(common.Interface2String(part["type"])) + partType := strings.TrimSpace(kitutil.Interface2String(part["type"])) var data string var mimeType string @@ -222,7 +222,7 @@ func ContentPartToFileSource(part map[string]any) types.FileSource { data, mimeType = responsesPartDataAndMime(part, "input_audio", "data", "url") if mimeType == "" { if payload, ok := part["input_audio"].(map[string]any); ok { - if format := strings.TrimSpace(common.Interface2String(payload["format"])); format != "" { + if format := strings.TrimSpace(kitutil.Interface2String(payload["format"])); format != "" { mimeType = "audio/" + format } } @@ -237,7 +237,7 @@ func ContentPartToFileSource(part map[string]any) types.FileSource { } func responsesPartDataAndMime(part map[string]any, keys ...string) (string, string) { - mimeType := strings.TrimSpace(common.Interface2String(part["mime_type"])) + mimeType := strings.TrimSpace(kitutil.Interface2String(part["mime_type"])) for _, key := range keys { value, ok := part[key] if !ok { @@ -250,10 +250,10 @@ func responsesPartDataAndMime(part map[string]any, keys ...string) (string, stri } case map[string]any: if mimeType == "" { - mimeType = strings.TrimSpace(common.Interface2String(typed["mime_type"])) + mimeType = strings.TrimSpace(kitutil.Interface2String(typed["mime_type"])) } for _, nestedKey := range []string{"url", "file_data", "file_url", "data"} { - if data := strings.TrimSpace(common.Interface2String(typed[nestedKey])); data != "" { + if data := strings.TrimSpace(kitutil.Interface2String(typed[nestedKey])); data != "" { return data, mimeType } } diff --git a/service/relayconvert/internal/oai_responses/to_claude_messages_req.go b/relaykit/relayconvert/internal/oai_responses/to_claude_messages_req.go similarity index 80% rename from service/relayconvert/internal/oai_responses/to_claude_messages_req.go rename to relaykit/relayconvert/internal/oai_responses/to_claude_messages_req.go index d376dce7..53aa674c 100644 --- a/service/relayconvert/internal/oai_responses/to_claude_messages_req.go +++ b/relaykit/relayconvert/internal/oai_responses/to_claude_messages_req.go @@ -4,24 +4,23 @@ import ( "fmt" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" - relaymedia "github.com/QuantumNous/new-api/service/relayconvert/internal/media" - sharedclaude "github.com/QuantumNous/new-api/service/relayconvert/internal/shared/claude" - "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/gin-gonic/gin" + "context" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + relaymedia "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/media" + sharedclaude "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/claude" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" ) -func convertOpenAIResponsesRequestToClaudeMessages(c *gin.Context, _ *relaycommon.RelayInfo, request any) (any, error) { +func convertOpenAIResponsesRequestToClaudeMessages(c context.Context, info convmeta.Meta, request any) (any, error) { responsesRequest, err := OpenAIResponsesRequestFromAny(request) if err != nil { return nil, err } - return OpenAIResponsesRequestToClaudeMessages(c, responsesRequest) + return OpenAIResponsesRequestToClaudeMessages(c, info, responsesRequest) } -func OpenAIResponsesRequestToClaudeMessages(c *gin.Context, req *dto.OpenAIResponsesRequest) (*dto.ClaudeRequest, error) { +func OpenAIResponsesRequestToClaudeMessages(c context.Context, info convmeta.Meta, req *dto.OpenAIResponsesRequest) (*dto.ClaudeRequest, error) { if req == nil { return nil, fmt.Errorf("request is nil") } @@ -39,11 +38,13 @@ func OpenAIResponsesRequestToClaudeMessages(c *gin.Context, req *dto.OpenAIRespo Stream: req.Stream, } if req.MaxOutputTokens != nil && *req.MaxOutputTokens > 0 { - claudeRequest.MaxTokens = common.GetPointer(*req.MaxOutputTokens) + claudeRequest.MaxTokens = kitutil.GetPointer(*req.MaxOutputTokens) } if claudeRequest.MaxTokens == nil || *claudeRequest.MaxTokens == 0 { - defaultMaxTokens := uint(model_setting.GetClaudeSettings().GetDefaultMaxTokens(req.Model)) - claudeRequest.MaxTokens = &defaultMaxTokens + if defaultMaxTokens, configured := convmeta.OptionsOf(info).Claude.DefaultMaxTokensFor(req.Model); configured { + value := uint(defaultMaxTokens) + claudeRequest.MaxTokens = &value + } } functions, err := RequestFunctionDeclarations(req.Tools) @@ -72,7 +73,7 @@ func OpenAIResponsesRequestToClaudeMessages(c *gin.Context, req *dto.OpenAIRespo if strings.TrimSpace(instructions) != "" { systemMessages = append(systemMessages, dto.ClaudeMediaMessage{ Type: "text", - Text: common.GetPointer(instructions), + Text: kitutil.GetPointer(instructions), }) } } @@ -82,7 +83,7 @@ func OpenAIResponsesRequestToClaudeMessages(c *gin.Context, req *dto.OpenAIRespo return nil, err } for _, item := range inputItems { - itemType := strings.TrimSpace(common.Interface2String(item["type"])) + itemType := strings.TrimSpace(kitutil.Interface2String(item["type"])) switch itemType { case ResponsesInputTypeFunctionCall: claudeRequest.Messages = appendClaudeToolUse(claudeRequest.Messages, responsesFunctionCallItemToClaudeToolUse(item, "arguments")) @@ -104,7 +105,7 @@ func OpenAIResponsesRequestToClaudeMessages(c *gin.Context, req *dto.OpenAIRespo parts = []dto.ClaudeMediaMessage{ { Type: "text", - Text: common.GetPointer("..."), + Text: kitutil.GetPointer("..."), }, } } @@ -119,6 +120,11 @@ func OpenAIResponsesRequestToClaudeMessages(c *gin.Context, req *dto.OpenAIRespo claudeRequest.System = systemMessages } claudeRequest.Messages = ensureClaudeMessagesStartWithUser(claudeRequest.Messages) + // Checked last so every injection path has had its chance to satisfy the + // required field. + if claudeRequest.MaxTokens == nil { + return nil, sharedclaude.ErrMissingMaxTokens + } return claudeRequest, nil } @@ -160,22 +166,22 @@ func applyResponsesReasoningToClaude(req *dto.OpenAIResponsesRequest, claudeRequ case "low": claudeRequest.Thinking = &dto.Thinking{ Type: "enabled", - BudgetTokens: common.GetPointer(1280), + BudgetTokens: kitutil.GetPointer(1280), } case "medium": claudeRequest.Thinking = &dto.Thinking{ Type: "enabled", - BudgetTokens: common.GetPointer(2048), + BudgetTokens: kitutil.GetPointer(2048), } case "high": claudeRequest.Thinking = &dto.Thinking{ Type: "enabled", - BudgetTokens: common.GetPointer(4096), + BudgetTokens: kitutil.GetPointer(4096), } } } -func responsesInputContentToClaudeMediaMessages(c *gin.Context, content any) ([]dto.ClaudeMediaMessage, error) { +func responsesInputContentToClaudeMediaMessages(c context.Context, content any) ([]dto.ClaudeMediaMessage, error) { contentParts, err := ContentParts(content) if err != nil { return nil, err @@ -183,14 +189,14 @@ func responsesInputContentToClaudeMediaMessages(c *gin.Context, content any) ([] parts := make([]dto.ClaudeMediaMessage, 0, len(contentParts)) for _, contentPart := range contentParts { - partType := strings.TrimSpace(common.Interface2String(contentPart["type"])) + partType := strings.TrimSpace(kitutil.Interface2String(contentPart["type"])) switch partType { case "input_text", "output_text", "text": - text := common.Interface2String(contentPart["text"]) + text := kitutil.Interface2String(contentPart["text"]) if text != "" { parts = append(parts, dto.ClaudeMediaMessage{ Type: "text", - Text: common.GetPointer(text), + Text: kitutil.GetPointer(text), }) } case "input_image", "input_file", "input_audio", "input_video": @@ -224,7 +230,7 @@ func responsesFunctionCallItemToClaudeToolUse(item map[string]any, inputKey stri return dto.ClaudeMediaMessage{ Type: "tool_use", Id: CallID(item), - Name: strings.TrimSpace(common.Interface2String(item["name"])), + Name: strings.TrimSpace(kitutil.Interface2String(item["name"])), Input: ObjectValue(item[inputKey], inputKey), } } @@ -285,17 +291,17 @@ func claudeMessageContentParts(content any) []dto.ClaudeMediaMessage { return []dto.ClaudeMediaMessage{ { Type: "text", - Text: common.GetPointer(typed), + Text: kitutil.GetPointer(typed), }, } default: - parts, _ := common.Any2Type[[]dto.ClaudeMediaMessage](content) + parts, _ := kitutil.Any2Type[[]dto.ClaudeMediaMessage](content) return parts } } func responsesClaudeRole(item map[string]any) string { - switch strings.TrimSpace(common.Interface2String(item["role"])) { + switch strings.TrimSpace(kitutil.Interface2String(item["role"])) { case "assistant": return "assistant" case "system", "developer": @@ -315,7 +321,7 @@ func ensureClaudeMessagesStartWithUser(messages []dto.ClaudeMessage) []dto.Claud Content: []dto.ClaudeMediaMessage{ { Type: "text", - Text: common.GetPointer("..."), + Text: kitutil.GetPointer("..."), }, }, }, diff --git a/service/relayconvert/internal/oai_responses/to_gemini_chat_req.go b/relaykit/relayconvert/internal/oai_responses/to_gemini_chat_req.go similarity index 79% rename from service/relayconvert/internal/oai_responses/to_gemini_chat_req.go rename to relaykit/relayconvert/internal/oai_responses/to_gemini_chat_req.go index 8810e987..18ddceed 100644 --- a/service/relayconvert/internal/oai_responses/to_gemini_chat_req.go +++ b/relaykit/relayconvert/internal/oai_responses/to_gemini_chat_req.go @@ -4,17 +4,15 @@ import ( "fmt" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" - relaymedia "github.com/QuantumNous/new-api/service/relayconvert/internal/media" - relaymeta "github.com/QuantumNous/new-api/service/relayconvert/internal/meta" - sharedgemini "github.com/QuantumNous/new-api/service/relayconvert/internal/shared/gemini" - "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/gin-gonic/gin" + "context" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + relaymedia "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/media" + sharedgemini "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/gemini" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" ) -func convertOpenAIResponsesRequestToGeminiChat(c *gin.Context, info *relaycommon.RelayInfo, request any) (any, error) { +func convertOpenAIResponsesRequestToGeminiChat(c context.Context, info convmeta.Meta, request any) (any, error) { responsesRequest, err := OpenAIResponsesRequestFromAny(request) if err != nil { return nil, err @@ -27,7 +25,8 @@ func convertOpenAIResponsesRequestToGeminiChat(c *gin.Context, info *relaycommon return OpenAIResponsesRequestToGeminiChat(c, &prepared, info) } -func OpenAIResponsesRequestToGeminiChat(c *gin.Context, req *dto.OpenAIResponsesRequest, info *relaycommon.RelayInfo) (*dto.GeminiChatRequest, error) { +func OpenAIResponsesRequestToGeminiChat(c context.Context, req *dto.OpenAIResponsesRequest, info convmeta.Meta) (*dto.GeminiChatRequest, error) { + opts := convmeta.OptionsOf(info) if req == nil { return nil, fmt.Errorf("request is nil") } @@ -44,17 +43,17 @@ func OpenAIResponsesRequestToGeminiChat(c *gin.Context, req *dto.OpenAIResponses }, } if req.TopP != nil && *req.TopP > 0 { - geminiRequest.GenerationConfig.TopP = common.GetPointer(*req.TopP) + geminiRequest.GenerationConfig.TopP = kitutil.GetPointer(*req.TopP) } if req.MaxOutputTokens != nil && *req.MaxOutputTokens > 0 { - geminiRequest.GenerationConfig.MaxOutputTokens = common.GetPointer(*req.MaxOutputTokens) + geminiRequest.GenerationConfig.MaxOutputTokens = kitutil.GetPointer(*req.MaxOutputTokens) } upstreamModelName := req.Model - if modelName := relaymeta.RelayInfoUpstreamModelName(info); modelName != "" { + if modelName := convmeta.UpstreamModelName(info); modelName != "" { upstreamModelName = modelName } - if model_setting.IsGeminiModelSupportImagine(upstreamModelName) { + if opts.Gemini.SupportsImagineModel(upstreamModelName) { geminiRequest.GenerationConfig.ResponseModalities = []string{"TEXT", "IMAGE"} } if err := applyResponsesTextToGemini(req.Text, geminiRequest); err != nil { @@ -66,14 +65,20 @@ func OpenAIResponsesRequestToGeminiChat(c *gin.Context, req *dto.OpenAIResponses ReasoningEffort: ReasoningEffort(req), }) - safetySettings := make([]dto.GeminiChatSafetySettings, 0, len(sharedgemini.SafetySettingCategories)) + var safetySettings []dto.GeminiChatSafetySettings for _, category := range sharedgemini.SafetySettingCategories { + threshold := opts.Gemini.SafetySettingFor(category) + if threshold == "" { + continue + } safetySettings = append(safetySettings, dto.GeminiChatSafetySettings{ Category: category, - Threshold: model_setting.GetGeminiSafetySetting(category), + Threshold: threshold, }) } - geminiRequest.SafetySettings = safetySettings + if len(safetySettings) > 0 { + geminiRequest.SafetySettings = safetySettings + } functions, err := RequestFunctionDeclarations(req.Tools) if err != nil { @@ -119,14 +124,14 @@ func OpenAIResponsesRequestToGeminiChat(c *gin.Context, req *dto.OpenAIResponses } callNames := make(map[string]string) for _, item := range inputItems { - itemType := strings.TrimSpace(common.Interface2String(item["type"])) + itemType := strings.TrimSpace(kitutil.Interface2String(item["type"])) switch itemType { case ResponsesInputTypeFunctionCall: part, callID, err := responsesFunctionCallItemToGeminiPart(item) if err != nil { return nil, err } - sharedgemini.AttachFunctionCallThoughtSignature(&part) + sharedgemini.AttachFunctionCallThoughtSignature(opts, &part) if callID != "" { callNames[callID] = part.FunctionCall.FunctionName } @@ -181,14 +186,14 @@ func applyResponsesTextToGemini(raw []byte, geminiRequest *dto.GeminiChatRequest } var jsonSchema dto.FormatJsonSchema - if err := common.Unmarshal(responseFormat.JsonSchema, &jsonSchema); err != nil { + if err := kitutil.Unmarshal(responseFormat.JsonSchema, &jsonSchema); err != nil { return nil } geminiRequest.GenerationConfig.ResponseSchema = sharedgemini.RemoveAdditionalProperties(jsonSchema.Schema, 0) return nil } -func responsesInputContentToGeminiParts(c *gin.Context, content any) ([]dto.GeminiPart, error) { +func responsesInputContentToGeminiParts(c context.Context, content any) ([]dto.GeminiPart, error) { contentParts, err := ContentParts(content) if err != nil { return nil, err @@ -205,11 +210,11 @@ func responsesInputContentToGeminiParts(c *gin.Context, content any) ([]dto.Gemi return parts, nil } -func responsesContentPartToGeminiParts(c *gin.Context, part map[string]any) ([]dto.GeminiPart, error) { - partType := strings.TrimSpace(common.Interface2String(part["type"])) +func responsesContentPartToGeminiParts(c context.Context, part map[string]any) ([]dto.GeminiPart, error) { + partType := strings.TrimSpace(kitutil.Interface2String(part["type"])) switch partType { case "input_text", "output_text", "text": - text := common.Interface2String(part["text"]) + text := kitutil.Interface2String(part["text"]) if text == "" { return nil, nil } @@ -240,7 +245,7 @@ func responsesContentPartToGeminiParts(c *gin.Context, part map[string]any) ([]d } func responsesFunctionCallItemToGeminiPart(item map[string]any) (dto.GeminiPart, string, error) { - name := strings.TrimSpace(common.Interface2String(item["name"])) + name := strings.TrimSpace(kitutil.Interface2String(item["name"])) if name == "" { return dto.GeminiPart{}, "", fmt.Errorf("function_call item is missing name") } @@ -255,7 +260,7 @@ func responsesFunctionCallItemToGeminiPart(item map[string]any) (dto.GeminiPart, func responsesFunctionOutputItemToGeminiPart(item map[string]any, callNames map[string]string) dto.GeminiPart { callID := CallID(item) - name := strings.TrimSpace(common.Interface2String(item["name"])) + name := strings.TrimSpace(kitutil.Interface2String(item["name"])) if name == "" { name = callNames[callID] } @@ -291,7 +296,7 @@ func appendGeminiContentPart(req *dto.GeminiChatRequest, role string, part dto.G } func responsesGeminiRole(item map[string]any) string { - switch strings.TrimSpace(common.Interface2String(item["role"])) { + switch strings.TrimSpace(kitutil.Interface2String(item["role"])) { case "assistant": return "model" case "system", "developer": diff --git a/service/relayconvert/internal/oai_responses/to_gemini_chat_req_preprocess.go b/relaykit/relayconvert/internal/oai_responses/to_gemini_chat_req_preprocess.go similarity index 65% rename from service/relayconvert/internal/oai_responses/to_gemini_chat_req_preprocess.go rename to relaykit/relayconvert/internal/oai_responses/to_gemini_chat_req_preprocess.go index 71d96451..77644654 100644 --- a/service/relayconvert/internal/oai_responses/to_gemini_chat_req_preprocess.go +++ b/relaykit/relayconvert/internal/oai_responses/to_gemini_chat_req_preprocess.go @@ -3,8 +3,8 @@ package oairesponses import ( "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" ) const ( @@ -34,18 +34,18 @@ func PrepareOpenAIResponsesRequest(request dto.OpenAIResponsesRequest) (dto.Open } func filterGeminiResponsesTools(raw []byte) ([]byte, error) { - if !geminiRawJSONPresent(raw) || common.GetJsonType(raw) != "array" { + if !geminiRawJSONPresent(raw) || kitutil.GetJsonType(raw) != "array" { return raw, nil } var tools []map[string]any - if err := common.Unmarshal(raw, &tools); err != nil { + if err := kitutil.Unmarshal(raw, &tools); err != nil { return nil, err } filtered := make([]map[string]any, 0, len(tools)) for _, tool := range tools { - if strings.TrimSpace(common.Interface2String(tool["type"])) != "function" { + if strings.TrimSpace(kitutil.Interface2String(tool["type"])) != "function" { continue } filtered = append(filtered, tool) @@ -53,49 +53,49 @@ func filterGeminiResponsesTools(raw []byte) ([]byte, error) { if len(filtered) == 0 { return nil, nil } - return common.Marshal(filtered) + return kitutil.Marshal(filtered) } func filterGeminiResponsesInput(raw []byte) ([]byte, error) { - if !geminiRawJSONPresent(raw) || common.GetJsonType(raw) != "array" { + if !geminiRawJSONPresent(raw) || kitutil.GetJsonType(raw) != "array" { return raw, nil } var items []map[string]any - if err := common.Unmarshal(raw, &items); err != nil { + if err := kitutil.Unmarshal(raw, &items); err != nil { return nil, err } skippedCustomCallIDs := make(map[string]struct{}) for _, item := range items { - if strings.TrimSpace(common.Interface2String(item["type"])) != geminiResponsesInputTypeCustomToolCall { + if strings.TrimSpace(kitutil.Interface2String(item["type"])) != geminiResponsesInputTypeCustomToolCall { continue } - if callID := strings.TrimSpace(common.Interface2String(item["call_id"])); callID != "" { + if callID := strings.TrimSpace(kitutil.Interface2String(item["call_id"])); callID != "" { skippedCustomCallIDs[callID] = struct{}{} } } filtered := make([]map[string]any, 0, len(items)) for _, item := range items { - itemType := strings.TrimSpace(common.Interface2String(item["type"])) + itemType := strings.TrimSpace(kitutil.Interface2String(item["type"])) switch itemType { case geminiResponsesInputTypeCustomToolCall, geminiResponsesInputTypeCustomToolCallOutput: continue case geminiResponsesInputTypeFunctionCallOutput: - if _, ok := skippedCustomCallIDs[strings.TrimSpace(common.Interface2String(item["call_id"]))]; ok { + if _, ok := skippedCustomCallIDs[strings.TrimSpace(kitutil.Interface2String(item["call_id"]))]; ok { continue } } filtered = append(filtered, item) } - return common.Marshal(filtered) + return kitutil.Marshal(filtered) } func geminiRawJSONPresent(raw []byte) bool { if len(raw) == 0 { return false } - return common.GetJsonType(raw) != "null" + return kitutil.GetJsonType(raw) != "null" } diff --git a/service/relayconvert/internal/oai_responses/to_oai_chat_req.go b/relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go similarity index 83% rename from service/relayconvert/internal/oai_responses/to_oai_chat_req.go rename to relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go index 7779364a..263887a4 100644 --- a/service/relayconvert/internal/oai_responses/to_oai_chat_req.go +++ b/relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go @@ -6,8 +6,8 @@ import ( "fmt" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" ) const ( @@ -79,17 +79,17 @@ func ResponsesRequestToChatCompletionsRequest(req *dto.OpenAIResponsesRequest) ( out.ReasoningEffort = req.Reasoning.Effort } if req.ServiceTier != "" { - out.ServiceTier, _ = common.Marshal(req.ServiceTier) + out.ServiceTier, _ = kitutil.Marshal(req.ServiceTier) } - if len(req.ParallelToolCalls) > 0 && common.GetJsonType(req.ParallelToolCalls) == "boolean" { + if len(req.ParallelToolCalls) > 0 && kitutil.GetJsonType(req.ParallelToolCalls) == "boolean" { var parallelToolCalls bool - if err := common.Unmarshal(req.ParallelToolCalls, ¶llelToolCalls); err == nil { + if err := kitutil.Unmarshal(req.ParallelToolCalls, ¶llelToolCalls); err == nil { out.ParallelTooCalls = ¶llelToolCalls } } - if len(req.PromptCacheKey) > 0 && common.GetJsonType(req.PromptCacheKey) == "string" { + if len(req.PromptCacheKey) > 0 && kitutil.GetJsonType(req.PromptCacheKey) == "string" { var promptCacheKey string - if err := common.Unmarshal(req.PromptCacheKey, &promptCacheKey); err == nil { + if err := kitutil.Unmarshal(req.PromptCacheKey, &promptCacheKey); err == nil { out.PromptCacheKey = promptCacheKey } } @@ -137,7 +137,7 @@ func responsesRequestMessagesToChat(req *dto.OpenAIResponsesRequest) ([]dto.Mess return messages, nil } - switch common.GetJsonType(req.Input) { + switch kitutil.GetJsonType(req.Input) { case "string": input, err := responsesJSONString(req.Input) if err != nil { @@ -147,7 +147,7 @@ func responsesRequestMessagesToChat(req *dto.OpenAIResponsesRequest) ([]dto.Mess return messages, nil case "array": var items []map[string]any - if err := common.Unmarshal(req.Input, &items); err != nil { + if err := kitutil.Unmarshal(req.Input, &items); err != nil { return nil, fmt.Errorf("invalid input array: %w", err) } for _, item := range items { @@ -159,12 +159,12 @@ func responsesRequestMessagesToChat(req *dto.OpenAIResponsesRequest) ([]dto.Mess } return messages, nil default: - return nil, fmt.Errorf("unsupported responses input type %q", common.GetJsonType(req.Input)) + return nil, fmt.Errorf("unsupported responses input type %q", kitutil.GetJsonType(req.Input)) } } func responsesInputItemToChatMessages(item map[string]any, messages []dto.Message) ([]dto.Message, error) { - itemType := strings.TrimSpace(common.Interface2String(item["type"])) + itemType := strings.TrimSpace(kitutil.Interface2String(item["type"])) switch itemType { case responsesInputTypeFunctionCall: toolCall, err := responsesFunctionCallItemToChatToolCall(item) @@ -179,12 +179,12 @@ func responsesInputItemToChatMessages(item map[string]any, messages []dto.Messag } return appendToolCallToLastAssistant(messages, toolCall), nil case responsesInputTypeFunctionCallOutput: - callID := strings.TrimSpace(common.Interface2String(item["call_id"])) + callID := strings.TrimSpace(kitutil.Interface2String(item["call_id"])) content := responseToolOutputToChatContent(item["output"]) return append(messages, dto.Message{Role: "tool", ToolCallId: callID, Content: content}), nil } - role := strings.TrimSpace(common.Interface2String(item["role"])) + role := strings.TrimSpace(kitutil.Interface2String(item["role"])) if role == "" { role = "user" } @@ -229,10 +229,10 @@ func responsesContentPartsToChatContent(parts []any) (any, error) { continue } - partType := strings.TrimSpace(common.Interface2String(part["type"])) + partType := strings.TrimSpace(kitutil.Interface2String(part["type"])) switch partType { case "input_text", "output_text", "text": - text := common.Interface2String(part["text"]) + text := kitutil.Interface2String(part["text"]) textOnly.WriteString(text) chatParts = append(chatParts, map[string]any{ "type": dto.ContentTypeText, @@ -275,7 +275,7 @@ func responsesContentPartsToChatContent(parts []any) (any, error) { } func responsesFunctionCallItemToChatToolCall(item map[string]any) (dto.ToolCallRequest, error) { - name := strings.TrimSpace(common.Interface2String(item["name"])) + name := strings.TrimSpace(kitutil.Interface2String(item["name"])) if name == "" { return dto.ToolCallRequest{}, errors.New("function_call item is missing name") } @@ -290,7 +290,7 @@ func responsesFunctionCallItemToChatToolCall(item map[string]any) (dto.ToolCallR } func responsesCustomToolCallItemToChatToolCall(item map[string]any) (dto.ToolCallRequest, error) { - raw, err := common.Marshal(item) + raw, err := kitutil.Marshal(item) if err != nil { return dto.ToolCallRequest{}, err } @@ -299,7 +299,7 @@ func responsesCustomToolCallItemToChatToolCall(item map[string]any) (dto.ToolCal Type: dto.CustomType, Custom: raw, Function: dto.FunctionRequest{ - Name: strings.TrimSpace(common.Interface2String(item["name"])), + Name: strings.TrimSpace(kitutil.Interface2String(item["name"])), Arguments: responsesArgumentsString(item["input"]), }, }, nil @@ -313,7 +313,7 @@ func appendToolCallToLastAssistant(messages []dto.Message, toolCall dto.ToolCall idx := len(messages) - 1 toolCalls := messages[idx].ParseToolCalls() toolCalls = append(toolCalls, toolCall) - toolCallsRaw, _ := common.Marshal(toolCalls) + toolCallsRaw, _ := kitutil.Marshal(toolCalls) messages[idx].ToolCalls = toolCallsRaw return messages } @@ -324,26 +324,26 @@ func responsesRequestToolsToChat(raw json.RawMessage) ([]dto.ToolCallRequest, er } var tools []map[string]any - if err := common.Unmarshal(raw, &tools); err != nil { + if err := kitutil.Unmarshal(raw, &tools); err != nil { return nil, fmt.Errorf("invalid tools: %w", err) } out := make([]dto.ToolCallRequest, 0, len(tools)) for _, tool := range tools { - toolType := strings.TrimSpace(common.Interface2String(tool["type"])) + toolType := strings.TrimSpace(kitutil.Interface2String(tool["type"])) if toolType == "function" { out = append(out, dto.ToolCallRequest{ Type: "function", Function: dto.FunctionRequest{ - Name: strings.TrimSpace(common.Interface2String(tool["name"])), - Description: common.Interface2String(tool["description"]), + Name: strings.TrimSpace(kitutil.Interface2String(tool["name"])), + Description: kitutil.Interface2String(tool["description"]), Parameters: tool["parameters"], }, }) continue } - rawTool, err := common.Marshal(tool) + rawTool, err := kitutil.Marshal(tool) if err != nil { return nil, err } @@ -359,20 +359,20 @@ func responsesRequestToolChoiceToChat(raw json.RawMessage) (any, error) { if !rawJSONPresent(raw) { return nil, nil } - if common.GetJsonType(raw) == "string" { + if kitutil.GetJsonType(raw) == "string" { var choice string - if err := common.Unmarshal(raw, &choice); err != nil { + if err := kitutil.Unmarshal(raw, &choice); err != nil { return nil, fmt.Errorf("invalid tool_choice: %w", err) } return choice, nil } var choice map[string]any - if err := common.Unmarshal(raw, &choice); err != nil { + if err := kitutil.Unmarshal(raw, &choice); err != nil { return nil, fmt.Errorf("invalid tool_choice: %w", err) } - if common.Interface2String(choice["type"]) == "function" { - name := strings.TrimSpace(common.Interface2String(choice["name"])) + if kitutil.Interface2String(choice["type"]) == "function" { + name := strings.TrimSpace(kitutil.Interface2String(choice["name"])) if name != "" { return map[string]any{ "type": "function", @@ -395,7 +395,7 @@ func responsesRequestTextToChatResponseFormat(raw json.RawMessage) (*dto.Respons } var textConfig map[string]any - if err := common.Unmarshal(raw, &textConfig); err != nil { + if err := kitutil.Unmarshal(raw, &textConfig); err != nil { return nil, fmt.Errorf("invalid text config: %w", err) } format, ok := textConfig["format"].(map[string]any) @@ -403,14 +403,14 @@ func responsesRequestTextToChatResponseFormat(raw json.RawMessage) (*dto.Respons return nil, nil } - formatType := strings.TrimSpace(common.Interface2String(format["type"])) + formatType := strings.TrimSpace(kitutil.Interface2String(format["type"])) if formatType == "" { return nil, nil } out := &dto.ResponseFormat{Type: formatType} if formatType == "json_schema" { - schemaRaw, err := common.Marshal(format) + schemaRaw, err := kitutil.Marshal(format) if err != nil { return nil, err } @@ -458,13 +458,13 @@ func responsesFilePartToChatFile(part map[string]any) any { func responsesVideoPartToChatVideoURL(part map[string]any) any { if videoURL, ok := part["video_url"]; ok { if videoURLMap, ok := videoURL.(map[string]any); ok { - if url := common.Interface2String(videoURLMap["url"]); url != "" { + if url := kitutil.Interface2String(videoURLMap["url"]); url != "" { return url } } return videoURL } - if url := common.Interface2String(part["url"]); url != "" { + if url := kitutil.Interface2String(part["url"]); url != "" { return url } return responsesPartPayload(part, "video_url") @@ -485,11 +485,11 @@ func responsesPartPayload(part map[string]any, key string) any { } func responsesCallID(item map[string]any) string { - callID := strings.TrimSpace(common.Interface2String(item["call_id"])) + callID := strings.TrimSpace(kitutil.Interface2String(item["call_id"])) if callID != "" { return callID } - return strings.TrimSpace(common.Interface2String(item["id"])) + return strings.TrimSpace(kitutil.Interface2String(item["id"])) } func CallID(item map[string]any) string { @@ -503,9 +503,9 @@ func responsesArgumentsString(value any) string { case string: return v default: - raw, err := common.Marshal(v) + raw, err := kitutil.Marshal(v) if err != nil { - return common.Interface2String(v) + return kitutil.Interface2String(v) } return string(raw) } @@ -518,7 +518,7 @@ func responseToolOutputToChatContent(value any) any { case string: return v default: - raw, err := common.Marshal(v) + raw, err := kitutil.Marshal(v) if err != nil { return fmt.Sprintf("%v", v) } @@ -527,11 +527,11 @@ func responseToolOutputToChatContent(value any) any { } func responsesJSONString(raw json.RawMessage) (string, error) { - if common.GetJsonType(raw) != "string" { + if kitutil.GetJsonType(raw) != "string" { return string(raw), nil } var value string - if err := common.Unmarshal(raw, &value); err != nil { + if err := kitutil.Unmarshal(raw, &value); err != nil { return "", err } return value, nil @@ -541,7 +541,7 @@ func rawJSONPresent(raw json.RawMessage) bool { if len(raw) == 0 { return false } - return common.GetJsonType(raw) != "null" + return kitutil.GetJsonType(raw) != "null" } func JSONString(raw json.RawMessage) (string, error) { diff --git a/service/relayconvert/internal/oai_responses/to_oai_chat_req_test.go b/relaykit/relayconvert/internal/oai_responses/to_oai_chat_req_test.go similarity index 98% rename from service/relayconvert/internal/oai_responses/to_oai_chat_req_test.go rename to relaykit/relayconvert/internal/oai_responses/to_oai_chat_req_test.go index 6924c82a..a6f778f6 100644 --- a/service/relayconvert/internal/oai_responses/to_oai_chat_req_test.go +++ b/relaykit/relayconvert/internal/oai_responses/to_oai_chat_req_test.go @@ -3,8 +3,8 @@ package oairesponses import ( "testing" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -264,7 +264,7 @@ func TestResponsesRequestToChatCompletionsRequestRejectsStatefulFields(t *testin func mustRawMessage(t *testing.T, value any) []byte { t.Helper() - raw, err := common.Marshal(value) + raw, err := kitutil.Marshal(value) require.NoError(t, err) return raw } diff --git a/service/relayconvert/internal/oai_responses/to_oai_chat_resp.go b/relaykit/relayconvert/internal/oai_responses/to_oai_chat_resp.go similarity index 98% rename from service/relayconvert/internal/oai_responses/to_oai_chat_resp.go rename to relaykit/relayconvert/internal/oai_responses/to_oai_chat_resp.go index 9fcb3a1d..b97f9116 100644 --- a/service/relayconvert/internal/oai_responses/to_oai_chat_resp.go +++ b/relaykit/relayconvert/internal/oai_responses/to_oai_chat_resp.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" ) const ( @@ -233,7 +233,7 @@ func responseStatusString(resp *dto.OpenAIResponsesResponse) string { return "" } var status string - _ = common.Unmarshal(resp.Status, &status) + _ = kitutil.Unmarshal(resp.Status, &status) return strings.TrimSpace(status) } diff --git a/service/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go b/relaykit/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go similarity index 99% rename from service/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go rename to relaykit/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go index 39db54d0..49efa07d 100644 --- a/service/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go +++ b/relaykit/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go @@ -3,7 +3,7 @@ package oairesponses import ( "testing" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/service/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go b/relaykit/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go similarity index 98% rename from service/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go rename to relaykit/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go index 675e42d9..6026e389 100644 --- a/service/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go +++ b/relaykit/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go @@ -6,8 +6,8 @@ import ( "strings" "time" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" ) type ResponsesToChatStreamState struct { @@ -143,7 +143,7 @@ func (s *ResponsesToChatStreamState) ensureStart() []dto.ChatCompletionsStreamRe s.sentStart = true return []dto.ChatCompletionsStreamResponse{s.makeChunk(dto.ChatCompletionsStreamResponseChoiceDelta{ Role: "assistant", - Content: common.GetPointer(""), + Content: kitutil.GetPointer(""), }, nil)} } @@ -642,7 +642,7 @@ func (a *ResponsesBufferedAccumulator) BuildOutput() []dto.ResponsesOutput { if tool == nil { continue } - argsRaw, _ := common.Marshal(tool.Arguments.String()) + argsRaw, _ := kitutil.Marshal(tool.Arguments.String()) out = append(out, dto.ResponsesOutput{ Type: responsesOutputTypeFunctionCall, ID: tool.ItemID, diff --git a/service/relayconvert/internal/shared/claude/cache.go b/relaykit/relayconvert/internal/shared/claude/cache.go similarity index 100% rename from service/relayconvert/internal/shared/claude/cache.go rename to relaykit/relayconvert/internal/shared/claude/cache.go diff --git a/relaykit/relayconvert/internal/shared/claude/errors.go b/relaykit/relayconvert/internal/shared/claude/errors.go new file mode 100644 index 00000000..65b7aa41 --- /dev/null +++ b/relaykit/relayconvert/internal/shared/claude/errors.go @@ -0,0 +1,10 @@ +package claude + +import "errors" + +// ErrMissingMaxTokens is returned when an OpenAI-format request carries no +// usable max_tokens and no Options.Claude.DefaultMaxTokens hook is +// configured. The Claude Messages API rejects requests without max_tokens +// (400 "max_tokens: Field required"), so conversion fails loudly instead of +// emitting a request the upstream is guaranteed to refuse. +var ErrMissingMaxTokens = errors.New("claude messages request requires max_tokens: set max_tokens on the request or configure Options.Claude.DefaultMaxTokens") diff --git a/service/relayconvert/internal/shared/claude/tool_choice.go b/relaykit/relayconvert/internal/shared/claude/tool_choice.go similarity index 95% rename from service/relayconvert/internal/shared/claude/tool_choice.go rename to relaykit/relayconvert/internal/shared/claude/tool_choice.go index 88ba3061..64fcb082 100644 --- a/service/relayconvert/internal/shared/claude/tool_choice.go +++ b/relaykit/relayconvert/internal/shared/claude/tool_choice.go @@ -1,6 +1,6 @@ package claude -import "github.com/QuantumNous/new-api/dto" +import "github.com/QuantumNous/new-api/relaykit/dto" func MapOpenAIToolChoice(toolChoice any, parallelToolCalls *bool) *dto.ClaudeToolChoice { var claudeToolChoice *dto.ClaudeToolChoice diff --git a/service/relayconvert/internal/shared/gemini/request.go b/relaykit/relayconvert/internal/shared/gemini/request.go similarity index 81% rename from service/relayconvert/internal/shared/gemini/request.go rename to relaykit/relayconvert/internal/shared/gemini/request.go index 795b1375..ac4ce3a6 100644 --- a/service/relayconvert/internal/shared/gemini/request.go +++ b/relaykit/relayconvert/internal/shared/gemini/request.go @@ -4,12 +4,10 @@ import ( "strconv" "strings" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" - relaymeta "github.com/QuantumNous/new-api/service/relayconvert/internal/meta" - "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/QuantumNous/new-api/setting/reasoning" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" + "github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning" ) var SupportedMimeTypes = map[string]bool{ @@ -51,27 +49,27 @@ const ( flash25LiteMaxBudget = 24576 ) -func ShouldAttachThoughtSignature() bool { - return model_setting.GetGeminiSettings().FunctionCallThoughtSignatureEnabled +func ShouldAttachThoughtSignature(opts *convmeta.Options) bool { + return opts != nil && opts.Gemini.FunctionCallThoughtSignatureEnabled } -func AttachThoughtSignatureBypass(part *dto.GeminiPart) bool { - if part == nil || len(part.ThoughtSignature) > 0 || !ShouldAttachThoughtSignature() { +func AttachThoughtSignatureBypass(opts *convmeta.Options, part *dto.GeminiPart) bool { + if part == nil || len(part.ThoughtSignature) > 0 || !ShouldAttachThoughtSignature(opts) { return false } part.ThoughtSignature = []byte(strconv.Quote(ThoughtSignatureBypassValue)) return true } -func AttachFunctionCallThoughtSignature(part *dto.GeminiPart) bool { +func AttachFunctionCallThoughtSignature(opts *convmeta.Options, part *dto.GeminiPart) bool { if part == nil || !HasFunctionCallContent(part.FunctionCall) { return false } - return AttachThoughtSignatureBypass(part) + return AttachThoughtSignatureBypass(opts, part) } -func AttachFirstTextThoughtSignature(parts []dto.GeminiPart) bool { - if !ShouldAttachThoughtSignature() { +func AttachFirstTextThoughtSignature(opts *convmeta.Options, parts []dto.GeminiPart) bool { + if !ShouldAttachThoughtSignature(opts) { return false } for i := range parts { @@ -83,12 +81,13 @@ func AttachFirstTextThoughtSignature(parts []dto.GeminiPart) bool { return false } -func ApplyThinkingConfig(geminiRequest *dto.GeminiChatRequest, info *relaycommon.RelayInfo, oaiRequest ...dto.GeneralOpenAIRequest) { - if geminiRequest == nil || info == nil || !model_setting.GetGeminiSettings().ThinkingAdapterEnabled { +func ApplyThinkingConfig(geminiRequest *dto.GeminiChatRequest, info convmeta.Meta, oaiRequest ...dto.GeneralOpenAIRequest) { + opts := convmeta.OptionsOf(info) + if geminiRequest == nil || info == nil || !opts.Gemini.ThinkingAdapterEnabled { return } - modelName := relaymeta.RelayInfoUpstreamModelName(info) + modelName := convmeta.UpstreamModelName(info) isNew25Pro := strings.HasPrefix(modelName, "gemini-2.5-pro") && !strings.HasPrefix(modelName, "gemini-2.5-pro-preview-05-06") && !strings.HasPrefix(modelName, "gemini-2.5-pro-preview-03-25") @@ -99,7 +98,7 @@ func ApplyThinkingConfig(geminiRequest *dto.GeminiChatRequest, info *relaycommon if budgetTokens, err := strconv.Atoi(parts[1]); err == nil { clampedBudget := clampThinkingBudget(modelName, budgetTokens) geminiRequest.GenerationConfig.ThinkingConfig = &dto.GeminiThinkingConfig{ - ThinkingBudget: common.GetPointer(clampedBudget), + ThinkingBudget: kitutil.GetPointer(clampedBudget), IncludeThoughts: true, } } @@ -126,17 +125,17 @@ func ApplyThinkingConfig(geminiRequest *dto.GeminiChatRequest, info *relaycommon IncludeThoughts: true, } if geminiRequest.GenerationConfig.MaxOutputTokens != nil && *geminiRequest.GenerationConfig.MaxOutputTokens > 0 { - budgetTokens := model_setting.GetGeminiSettings().ThinkingAdapterBudgetTokensPercentage * float64(*geminiRequest.GenerationConfig.MaxOutputTokens) + budgetTokens := opts.Gemini.ThinkingAdapterBudgetTokensPercentage * float64(*geminiRequest.GenerationConfig.MaxOutputTokens) clampedBudget := clampThinkingBudget(modelName, int(budgetTokens)) - geminiRequest.GenerationConfig.ThinkingConfig.ThinkingBudget = common.GetPointer(clampedBudget) + geminiRequest.GenerationConfig.ThinkingConfig.ThinkingBudget = kitutil.GetPointer(clampedBudget) } else if len(oaiRequest) > 0 { - geminiRequest.GenerationConfig.ThinkingConfig.ThinkingBudget = common.GetPointer(clampThinkingBudgetByEffort(modelName, oaiRequest[0].ReasoningEffort)) + geminiRequest.GenerationConfig.ThinkingConfig.ThinkingBudget = kitutil.GetPointer(clampThinkingBudgetByEffort(modelName, oaiRequest[0].ReasoningEffort)) } } } else if strings.HasSuffix(modelName, "-nothinking") { if !isNew25Pro { geminiRequest.GenerationConfig.ThinkingConfig = &dto.GeminiThinkingConfig{ - ThinkingBudget: common.GetPointer(0), + ThinkingBudget: kitutil.GetPointer(0), } } } else if _, level, ok := reasoning.TrimEffortSuffix(modelName); ok && level != "" { @@ -144,7 +143,7 @@ func ApplyThinkingConfig(geminiRequest *dto.GeminiChatRequest, info *relaycommon IncludeThoughts: true, ThinkingLevel: level, } - info.ReasoningEffort = level + info.SetReasoningEffort(level) } } diff --git a/service/relayconvert/internal/shared/gemini/schema.go b/relaykit/relayconvert/internal/shared/gemini/schema.go similarity index 99% rename from service/relayconvert/internal/shared/gemini/schema.go rename to relaykit/relayconvert/internal/shared/gemini/schema.go index 692380fd..75cc5034 100644 --- a/service/relayconvert/internal/shared/gemini/schema.go +++ b/relaykit/relayconvert/internal/shared/gemini/schema.go @@ -3,7 +3,7 @@ package gemini import ( "strings" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" ) var geminiOpenAPISchemaAllowedFields = map[string]struct{}{ diff --git a/relaykit/relayconvert/kitutil/json.go b/relaykit/relayconvert/kitutil/json.go new file mode 100644 index 00000000..3236150b --- /dev/null +++ b/relaykit/relayconvert/kitutil/json.go @@ -0,0 +1,86 @@ +// Package kitutil holds the dependency-free helpers shared by the conversion +// kit packages (dto, types, relayconvert). It moved out of the host's common +// package as part of the relaykit extraction; common re-exports these for +// host code. +package kitutil + +import ( + "bytes" + "encoding/json" + "io" + "unsafe" +) + +func Unmarshal(data []byte, v any) error { + return json.Unmarshal(data, v) +} + +func UnmarshalJsonStr(data string, v any) error { + return json.Unmarshal(StringToByteSlice(data), v) +} + +func DecodeJson(reader io.Reader, v any) error { + return json.NewDecoder(reader).Decode(v) +} + +func Marshal(v any) ([]byte, error) { + return json.Marshal(v) +} + +func GetJsonType(data json.RawMessage) string { + trimmed := bytes.TrimSpace(data) + if len(trimmed) == 0 { + return "unknown" + } + firstChar := trimmed[0] + switch firstChar { + case '{': + return "object" + case '[': + return "array" + case '"': + return "string" + case 't', 'f': + return "boolean" + case 'n': + return "null" + default: + return "number" + } +} + +// JsonRawMessageToString returns JSON strings as their decoded value and other JSON values as raw text. +func JsonRawMessageToString(data json.RawMessage) string { + trimmed := bytes.TrimSpace(data) + if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) { + return "" + } + if trimmed[0] != '"' { + return string(trimmed) + } + var value string + if err := Unmarshal(trimmed, &value); err != nil { + return string(trimmed) + } + return value +} + +func StringToByteSlice(s string) []byte { + tmp1 := (*[2]uintptr)(unsafe.Pointer(&s)) + tmp2 := [3]uintptr{tmp1[0], tmp1[1], tmp1[1]} + return *(*[]byte)(unsafe.Pointer(&tmp2)) +} + +func Any2Type[T any](data any) (T, error) { + var zero T + bytes, err := json.Marshal(data) + if err != nil { + return zero, err + } + var res T + err = json.Unmarshal(bytes, &res) + if err != nil { + return zero, err + } + return res, nil +} diff --git a/relaykit/relayconvert/kitutil/log.go b/relaykit/relayconvert/kitutil/log.go new file mode 100644 index 00000000..3b81d6f3 --- /dev/null +++ b/relaykit/relayconvert/kitutil/log.go @@ -0,0 +1,65 @@ +package kitutil + +import ( + "fmt" + "os" + "sync/atomic" +) + +// Kit packages log rare data-shape anomalies through these hooks. The host +// redirects them into its logging system at startup; standalone relaykit users +// get stderr defaults. + +type LogFunc func(message string) + +var ( + logInfo atomic.Pointer[LogFunc] + logError atomic.Pointer[LogFunc] + logSystemError atomic.Pointer[LogFunc] +) + +func SetLogging(info LogFunc, errorFn LogFunc) { + if info != nil { + logInfo.Store(&info) + } + if errorFn != nil { + logError.Store(&errorFn) + } +} + +// SetSystemErrorLogging configures the hook for internal converter failures. +func SetSystemErrorLogging(errorFn LogFunc) { + if errorFn != nil { + logSystemError.Store(&errorFn) + } +} + +func LogInfo(message string) { + if fn := logInfo.Load(); fn != nil { + (*fn)(message) + return + } + fmt.Fprintf(os.Stderr, "[relaykit] %s\n", message) +} + +func LogError(message string) { + if fn := logError.Load(); fn != nil { + (*fn)(message) + return + } + fmt.Fprintf(os.Stderr, "[relaykit] ERROR %s\n", message) +} + +// LogSystemError reports an internal converter failure through its dedicated +// hook, keeping it distinct from malformed request-data diagnostics. +func LogSystemError(message string) { + if fn := logSystemError.Load(); fn != nil { + (*fn)(message) + return + } + fmt.Fprintf(os.Stderr, "[relaykit] SYSTEM ERROR %s\n", message) +} + +// Debug reports whether verbose kit diagnostics are enabled. The host sets +// this once at startup (new-api mirrors common.DebugEnabled into it). +var Debug atomic.Bool diff --git a/relaykit/relayconvert/kitutil/log_test.go b/relaykit/relayconvert/kitutil/log_test.go new file mode 100644 index 00000000..4383fa84 --- /dev/null +++ b/relaykit/relayconvert/kitutil/log_test.go @@ -0,0 +1,31 @@ +package kitutil + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestErrorHooksRemainDistinct(t *testing.T) { + previousError := logError.Load() + previousSystemError := logSystemError.Load() + t.Cleanup(func() { + logError.Store(previousError) + logSystemError.Store(previousSystemError) + }) + + var ordinaryMessages []string + var systemMessages []string + SetLogging(nil, func(message string) { + ordinaryMessages = append(ordinaryMessages, message) + }) + SetSystemErrorLogging(func(message string) { + systemMessages = append(systemMessages, message) + }) + + LogError("invalid dto") + LogSystemError("converter failure") + + assert.Equal(t, []string{"invalid dto"}, ordinaryMessages) + assert.Equal(t, []string{"converter failure"}, systemMessages) +} diff --git a/relaykit/relayconvert/kitutil/mask.go b/relaykit/relayconvert/kitutil/mask.go new file mode 100644 index 00000000..d193a56e --- /dev/null +++ b/relaykit/relayconvert/kitutil/mask.go @@ -0,0 +1,134 @@ +package kitutil + +import ( + "net/url" + "regexp" + "strings" +) + +var ( + maskURLPattern = regexp.MustCompile(`(http|https)://[^\s/$.?#].[^\s]*`) + maskDomainPattern = regexp.MustCompile(`\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}\b`) + maskIPPattern = regexp.MustCompile(`\b(?:\d{1,3}\.){3}\d{1,3}\b`) + // maskApiKeyPattern matches patterns like 'api_key:xxx' or "api_key:xxx" to mask the API key value + maskApiKeyPattern = regexp.MustCompile(`(['"]?)api_key:([^\s'"]+)(['"]?)`) +) + +// maskHostTail returns the tail parts of a domain/host that should be preserved. +// It keeps 2 parts for likely country-code TLDs (e.g., co.uk, com.cn), otherwise keeps only the TLD. +func maskHostTail(parts []string) []string { + if len(parts) < 2 { + return parts + } + lastPart := parts[len(parts)-1] + secondLastPart := parts[len(parts)-2] + if len(lastPart) == 2 && len(secondLastPart) <= 3 { + // Likely country code TLD like co.uk, com.cn + return []string{secondLastPart, lastPart} + } + return []string{lastPart} +} + +// maskHostForURL collapses subdomains and keeps only masked prefix + preserved tail. +// Example: api.openai.com -> ***.com, sub.domain.co.uk -> ***.co.uk +func maskHostForURL(host string) string { + parts := strings.Split(host, ".") + if len(parts) < 2 { + return "***" + } + tail := maskHostTail(parts) + return "***." + strings.Join(tail, ".") +} + +// maskHostForPlainDomain masks a plain domain and reflects subdomain depth with multiple ***. +// Example: openai.com -> ***.com, api.openai.com -> ***.***.com, sub.domain.co.uk -> ***.***.co.uk +func maskHostForPlainDomain(domain string) string { + parts := strings.Split(domain, ".") + if len(parts) < 2 { + return domain + } + tail := maskHostTail(parts) + numStars := len(parts) - len(tail) + if numStars < 1 { + numStars = 1 + } + stars := strings.TrimSuffix(strings.Repeat("***.", numStars), ".") + return stars + "." + strings.Join(tail, ".") +} + +// MaskSensitiveInfo masks sensitive information like URLs, IPs, and domain names in a string +// Example: +// http://example.com -> http://***.com +// https://api.test.org/v1/users/123?key=secret -> https://***.org/***/***/?key=*** +// https://sub.domain.co.uk/path/to/resource -> https://***.co.uk/***/*** +// 192.168.1.1 -> ***.***.***.*** +// openai.com -> ***.com +// www.openai.com -> ***.***.com +// api.openai.com -> ***.***.com +func MaskSensitiveInfo(str string) string { + // Mask URLs + str = maskURLPattern.ReplaceAllStringFunc(str, func(urlStr string) string { + u, err := url.Parse(urlStr) + if err != nil { + return urlStr + } + + host := u.Host + if host == "" { + return urlStr + } + + // Mask host with unified logic + maskedHost := maskHostForURL(host) + + result := u.Scheme + "://" + maskedHost + + // Mask path + if u.Path != "" && u.Path != "/" { + pathParts := strings.Split(strings.Trim(u.Path, "/"), "/") + maskedPathParts := make([]string, len(pathParts)) + for i := range pathParts { + if pathParts[i] != "" { + maskedPathParts[i] = "***" + } + } + if len(maskedPathParts) > 0 { + result += "/" + strings.Join(maskedPathParts, "/") + } + } else if u.Path == "/" { + result += "/" + } + + // Mask query parameters + if u.RawQuery != "" { + values, err := url.ParseQuery(u.RawQuery) + if err != nil { + // If can't parse query, just mask the whole query string + result += "?***" + } else { + maskedParams := make([]string, 0, len(values)) + for key := range values { + maskedParams = append(maskedParams, key+"=***") + } + if len(maskedParams) > 0 { + result += "?" + strings.Join(maskedParams, "&") + } + } + } + + return result + }) + + // Mask domain names without protocol (like openai.com, www.openai.com) + str = maskDomainPattern.ReplaceAllStringFunc(str, func(domain string) string { + return maskHostForPlainDomain(domain) + }) + + // Mask IP addresses + str = maskIPPattern.ReplaceAllString(str, "***.***.***.***") + + // Mask API keys (e.g., "api_key:AIzaSyAAAaUooTUni8AdaOkSRMda30n_Q4vrV70" -> "api_key:***") + str = maskApiKeyPattern.ReplaceAllString(str, "${1}api_key:***${3}") + + return str +} diff --git a/relaykit/relayconvert/kitutil/value.go b/relaykit/relayconvert/kitutil/value.go new file mode 100644 index 00000000..745795cb --- /dev/null +++ b/relaykit/relayconvert/kitutil/value.go @@ -0,0 +1,52 @@ +package kitutil + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/google/uuid" +) + +func GetPointer[T any](v T) *T { + return &v +} + +func Interface2String(inter interface{}) string { + switch inter.(type) { + case string: + return inter.(string) + case int: + return fmt.Sprintf("%d", inter.(int)) + case float64: + return strconv.FormatFloat(inter.(float64), 'f', -1, 64) + case bool: + if inter.(bool) { + return "true" + } else { + return "false" + } + case nil: + return "" + } + return fmt.Sprintf("%v", inter) +} + +func String2Int(str string) int { + num, err := strconv.Atoi(str) + if err != nil { + return 0 + } + return num +} + +func GetUUID() string { + code := uuid.New().String() + code = strings.Replace(code, "-", "", -1) + return code +} + +func GetTimestamp() int64 { + return time.Now().Unix() +} diff --git a/service/relayconvert/media.go b/relaykit/relayconvert/media.go similarity index 64% rename from service/relayconvert/media.go rename to relaykit/relayconvert/media.go index d175e7e6..ab839079 100644 --- a/service/relayconvert/media.go +++ b/relaykit/relayconvert/media.go @@ -1,6 +1,6 @@ package relayconvert -import relaymedia "github.com/QuantumNous/new-api/service/relayconvert/internal/media" +import relaymedia "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/media" type MediaResolver = relaymedia.MediaResolver diff --git a/relaykit/relayconvert/reasoning/suffix.go b/relaykit/relayconvert/reasoning/suffix.go new file mode 100644 index 00000000..59140a7c --- /dev/null +++ b/relaykit/relayconvert/reasoning/suffix.go @@ -0,0 +1,51 @@ +package reasoning + +import ( + "strings" + + "github.com/samber/lo" +) + +var EffortSuffixes = []string{"-max", "-xhigh", "-high", "-medium", "-low", "-minimal"} + +var OpenAIEffortSuffixes = []string{"-high", "-minimal", "-low", "-medium", "-none", "-xhigh"} + +var DeepSeekV4EffortSuffixes = []string{"-none", "-max"} + +// TrimEffortSuffix -> modelName level(low) exists +func TrimEffortSuffix(modelName string) (string, string, bool) { + return TrimEffortSuffixWithSuffixes(modelName, EffortSuffixes) +} + +func TrimEffortSuffixWithSuffixes(modelName string, suffixes []string) (string, string, bool) { + suffix, found := lo.Find(suffixes, func(s string) bool { + return strings.HasSuffix(modelName, s) + }) + if !found { + return modelName, "", false + } + return strings.TrimSuffix(modelName, suffix), strings.TrimPrefix(suffix, "-"), true +} + +func ParseOpenAIReasoningEffortFromModelSuffix(modelName string) (string, string) { + baseModel, effort, ok := TrimEffortSuffixWithSuffixes(modelName, OpenAIEffortSuffixes) + if !ok { + return "", modelName + } + return effort, baseModel +} + +func ParseDeepSeekV4ThinkingSuffix(modelName string) (baseModel string, thinkingType string, effort string, ok bool) { + baseModel, suffix, ok := TrimEffortSuffixWithSuffixes(modelName, DeepSeekV4EffortSuffixes) + if !ok || !strings.HasPrefix(baseModel, "deepseek-v4-") { + return modelName, "", "", false + } + switch suffix { + case "none": + return baseModel, "disabled", "", true + case "max": + return baseModel, "enabled", "max", true + default: + return modelName, "", "", false + } +} diff --git a/relaykit/relayconvert/request_compat.go b/relaykit/relayconvert/request_compat.go new file mode 100644 index 00000000..fdacf68a --- /dev/null +++ b/relaykit/relayconvert/request_compat.go @@ -0,0 +1,48 @@ +package relayconvert + +import ( + "context" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + claudemessages "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/claude_messages" + geminichat "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/gemini_chat" + oaichat "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/oai_chat" + oairesponses "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/oai_responses" + sharedgemini "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/gemini" +) + +func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info convmeta.Meta) (*dto.GeneralOpenAIRequest, error) { + return claudemessages.ClaudeMessagesRequestToOpenAIChat(claudeRequest, info) +} + +func OpenAIChatRequestToClaudeMessages(c context.Context, info convmeta.Meta, textRequest dto.GeneralOpenAIRequest) (*dto.ClaudeRequest, error) { + return oaichat.OpenAIChatRequestToClaudeMessages(c, info, textRequest) +} + +func GeminiGenerateContentRequestToOpenAIChat(geminiRequest *dto.GeminiChatRequest, info convmeta.Meta) (*dto.GeneralOpenAIRequest, error) { + return geminichat.GeminiGenerateContentRequestToOpenAIChat(geminiRequest, info) +} + +func OpenAIChatRequestToGeminiGenerateContent(c context.Context, textRequest dto.GeneralOpenAIRequest, info convmeta.Meta) (*dto.GeminiChatRequest, error) { + return oaichat.OpenAIChatRequestToGeminiGenerateContent(c, textRequest, info) +} + +func ApplyGeminiThinkingConfig(geminiRequest *dto.GeminiChatRequest, info convmeta.Meta, oaiRequest ...dto.GeneralOpenAIRequest) { + sharedgemini.ApplyThinkingConfig(geminiRequest, info, oaiRequest...) +} + +func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*dto.OpenAIResponsesRequest, error) { + return oaichat.ChatCompletionsRequestToResponsesRequest(req) +} + +func ResponsesRequestToChatCompletionsRequest(req *dto.OpenAIResponsesRequest) (*dto.GeneralOpenAIRequest, error) { + return oairesponses.ResponsesRequestToChatCompletionsRequest(req) +} + +func OpenAIResponsesRequestToClaudeMessages(c context.Context, info convmeta.Meta, req *dto.OpenAIResponsesRequest) (*dto.ClaudeRequest, error) { + return oairesponses.OpenAIResponsesRequestToClaudeMessages(c, info, req) +} + +func OpenAIResponsesRequestToGeminiChat(c context.Context, req *dto.OpenAIResponsesRequest, info convmeta.Meta) (*dto.GeminiChatRequest, error) { + return oairesponses.OpenAIResponsesRequestToGeminiChat(c, req, info) +} diff --git a/service/relayconvert/request_registry.go b/relaykit/relayconvert/request_registry.go similarity index 84% rename from service/relayconvert/request_registry.go rename to relaykit/relayconvert/request_registry.go index c3b6eeed..e55d6c30 100644 --- a/service/relayconvert/request_registry.go +++ b/relaykit/relayconvert/request_registry.go @@ -7,17 +7,17 @@ import ( "strings" "sync" - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" - claudemessages "github.com/QuantumNous/new-api/service/relayconvert/internal/claude_messages" - geminichat "github.com/QuantumNous/new-api/service/relayconvert/internal/gemini_chat" - oaichat "github.com/QuantumNous/new-api/service/relayconvert/internal/oai_chat" - oairesponses "github.com/QuantumNous/new-api/service/relayconvert/internal/oai_responses" - "github.com/QuantumNous/new-api/types" - "github.com/gin-gonic/gin" + "context" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + claudemessages "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/claude_messages" + geminichat "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/gemini_chat" + oaichat "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/oai_chat" + oairesponses "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/oai_responses" + "github.com/QuantumNous/new-api/relaykit/types" ) -type RequestConverterFunc func(c *gin.Context, info *relaycommon.RelayInfo, request any) (any, error) +type RequestConverterFunc func(c context.Context, info convmeta.Meta, request any) (any, error) type RequestConverterQuality string @@ -148,7 +148,7 @@ func LookupRequestConverter(converter string) (RequestConverterSpec, bool) { return cloneRequestConverterSpec(spec), true } -func ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, target types.RelayFormat, request any) (*RequestResult, error) { +func ConvertRequest(c context.Context, info convmeta.Meta, target types.RelayFormat, request any) (*RequestResult, error) { from, err := inferRequestRelayFormat(request) if err != nil { return nil, err @@ -171,7 +171,7 @@ func ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, target types.Re return executeRequestSpec(c, info, from, target, request, spec) } -func ConvertRequestVia(c *gin.Context, info *relaycommon.RelayInfo, request any, path ...types.RelayFormat) (*RequestResult, error) { +func ConvertRequestVia(c context.Context, info convmeta.Meta, request any, path ...types.RelayFormat) (*RequestResult, error) { from, err := inferRequestRelayFormat(request) if err != nil { return nil, err @@ -211,7 +211,7 @@ func ConvertRequestVia(c *gin.Context, info *relaycommon.RelayInfo, request any, return executeRequestSteps(c, info, from, targets[len(targets)-1], request, "", "", steps) } -func ConvertRequestByID(c *gin.Context, info *relaycommon.RelayInfo, converter string, request any) (*RequestResult, error) { +func ConvertRequestByID(c context.Context, info convmeta.Meta, converter string, request any) (*RequestResult, error) { from, err := inferRequestRelayFormat(request) if err != nil { return nil, err @@ -227,7 +227,7 @@ func ConvertRequestByID(c *gin.Context, info *relaycommon.RelayInfo, converter s return executeRequestSpec(c, info, from, spec.To, request, spec) } -func executeRequestSpec(c *gin.Context, info *relaycommon.RelayInfo, from types.RelayFormat, target types.RelayFormat, request any, spec RequestConverterSpec) (*RequestResult, error) { +func executeRequestSpec(c context.Context, info convmeta.Meta, from types.RelayFormat, target types.RelayFormat, request any, spec RequestConverterSpec) (*RequestResult, error) { steps, err := expandRequestConverterSteps(spec) if err != nil { return nil, err @@ -235,7 +235,7 @@ func executeRequestSpec(c *gin.Context, info *relaycommon.RelayInfo, from types. return executeRequestSteps(c, info, from, target, request, spec.ID, spec.Quality, steps) } -func executeRequestSteps(c *gin.Context, info *relaycommon.RelayInfo, from types.RelayFormat, target types.RelayFormat, request any, converter string, quality RequestConverterQuality, specs []RequestConverterSpec) (*RequestResult, error) { +func executeRequestSteps(c context.Context, info convmeta.Meta, from types.RelayFormat, target types.RelayFormat, request any, converter string, quality RequestConverterQuality, specs []RequestConverterSpec) (*RequestResult, error) { current := request steps := make([]RequestStep, 0, len(specs)) for _, spec := range specs { @@ -303,7 +303,7 @@ func expandRequestConverterSteps(spec RequestConverterSpec) ([]RequestConverterS return steps, nil } -func executeRequestStep(c *gin.Context, info *relaycommon.RelayInfo, spec RequestConverterSpec, request any) (any, RequestStep, error) { +func executeRequestStep(c context.Context, info convmeta.Meta, spec RequestConverterSpec, request any) (any, RequestStep, error) { if spec.Convert == nil { return nil, RequestStep{}, fmt.Errorf("request converter %q has no registered implementation", spec.ID) } @@ -379,7 +379,7 @@ func inferRequestRelayFormat(request any) (types.RelayFormat, error) { if isNilRequest(request) { return "", errors.New("request is nil") } - format, ok := relaycommon.GuessRelayFormatFromRequest(request) + format, ok := convmeta.GuessRelayFormatFromRequest(request) if !ok { return "", fmt.Errorf("unsupported request type %T", request) } @@ -399,7 +399,7 @@ func isNilRequest(request any) bool { } } -func convertChatRequestToResponses(_ *gin.Context, _ *relaycommon.RelayInfo, request any) (any, error) { +func convertChatRequestToResponses(_ context.Context, _ convmeta.Meta, request any) (any, error) { chatRequest, ok := request.(*dto.GeneralOpenAIRequest) if !ok { if value, ok := request.(dto.GeneralOpenAIRequest); ok { @@ -412,7 +412,7 @@ func convertChatRequestToResponses(_ *gin.Context, _ *relaycommon.RelayInfo, req return oaichat.ChatCompletionsRequestToResponsesRequest(chatRequest) } -func convertClaudeRequestToOpenAI(_ *gin.Context, info *relaycommon.RelayInfo, request any) (any, error) { +func convertClaudeRequestToOpenAI(_ context.Context, info convmeta.Meta, request any) (any, error) { claudeRequest, ok := request.(*dto.ClaudeRequest) if !ok { if value, ok := request.(dto.ClaudeRequest); ok { @@ -425,7 +425,7 @@ func convertClaudeRequestToOpenAI(_ *gin.Context, info *relaycommon.RelayInfo, r return claudemessages.ClaudeMessagesRequestToOpenAIChat(*claudeRequest, info) } -func convertOpenAIRequestToClaude(c *gin.Context, _ *relaycommon.RelayInfo, request any) (any, error) { +func convertOpenAIRequestToClaude(c context.Context, info convmeta.Meta, request any) (any, error) { openAIRequest, ok := request.(*dto.GeneralOpenAIRequest) if !ok { if value, ok := request.(dto.GeneralOpenAIRequest); ok { @@ -435,10 +435,10 @@ func convertOpenAIRequestToClaude(c *gin.Context, _ *relaycommon.RelayInfo, requ if openAIRequest == nil { return nil, fmt.Errorf("expected OpenAI chat completions request, got %T", request) } - return oaichat.OpenAIChatRequestToClaudeMessages(c, *openAIRequest) + return oaichat.OpenAIChatRequestToClaudeMessages(c, info, *openAIRequest) } -func convertGeminiRequestToOpenAI(_ *gin.Context, info *relaycommon.RelayInfo, request any) (any, error) { +func convertGeminiRequestToOpenAI(_ context.Context, info convmeta.Meta, request any) (any, error) { geminiRequest, ok := request.(*dto.GeminiChatRequest) if !ok { if value, ok := request.(dto.GeminiChatRequest); ok { @@ -451,7 +451,7 @@ func convertGeminiRequestToOpenAI(_ *gin.Context, info *relaycommon.RelayInfo, r return geminichat.GeminiGenerateContentRequestToOpenAIChat(geminiRequest, info) } -func convertOpenAIRequestToGemini(c *gin.Context, info *relaycommon.RelayInfo, request any) (any, error) { +func convertOpenAIRequestToGemini(c context.Context, info convmeta.Meta, request any) (any, error) { openAIRequest, ok := request.(*dto.GeneralOpenAIRequest) if !ok { if value, ok := request.(dto.GeneralOpenAIRequest); ok { @@ -464,15 +464,15 @@ func convertOpenAIRequestToGemini(c *gin.Context, info *relaycommon.RelayInfo, r return oaichat.OpenAIChatRequestToGeminiGenerateContent(c, *openAIRequest, info) } -func convertOpenAIResponsesRequestToClaudeMessages(c *gin.Context, _ *relaycommon.RelayInfo, request any) (any, error) { +func convertOpenAIResponsesRequestToClaudeMessages(c context.Context, info convmeta.Meta, request any) (any, error) { responsesRequest, err := oairesponses.OpenAIResponsesRequestFromAny(request) if err != nil { return nil, err } - return oairesponses.OpenAIResponsesRequestToClaudeMessages(c, responsesRequest) + return oairesponses.OpenAIResponsesRequestToClaudeMessages(c, info, responsesRequest) } -func convertOpenAIResponsesRequestToGeminiChat(c *gin.Context, info *relaycommon.RelayInfo, request any) (any, error) { +func convertOpenAIResponsesRequestToGeminiChat(c context.Context, info convmeta.Meta, request any) (any, error) { responsesRequest, err := oairesponses.OpenAIResponsesRequestFromAny(request) if err != nil { return nil, err @@ -485,7 +485,7 @@ func convertOpenAIResponsesRequestToGeminiChat(c *gin.Context, info *relaycommon return oairesponses.OpenAIResponsesRequestToGeminiChat(c, &prepared, info) } -func convertResponsesRequestToChat(_ *gin.Context, _ *relaycommon.RelayInfo, request any) (any, error) { +func convertResponsesRequestToChat(_ context.Context, _ convmeta.Meta, request any) (any, error) { responsesRequest, ok := request.(*dto.OpenAIResponsesRequest) if !ok { if value, ok := request.(dto.OpenAIResponsesRequest); ok { diff --git a/service/relayconvert/request_registry_test.go b/relaykit/relayconvert/request_registry_test.go similarity index 80% rename from service/relayconvert/request_registry_test.go rename to relaykit/relayconvert/request_registry_test.go index 7785b69b..5649ed09 100644 --- a/service/relayconvert/request_registry_test.go +++ b/relaykit/relayconvert/request_registry_test.go @@ -3,13 +3,11 @@ package relayconvert import ( "testing" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" - sharedgemini "github.com/QuantumNous/new-api/service/relayconvert/internal/shared/gemini" - "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + sharedgemini "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/gemini" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -107,9 +105,8 @@ func TestRequestConverterRegistryListsSupportedTextConverters(t *testing.T) { } func TestConvertRequestToTargetRecordsConversionChain(t *testing.T) { - info := &relaycommon.RelayInfo{ - RelayFormat: types.RelayFormatOpenAI, - RequestConversionChain: []types.RelayFormat{types.RelayFormatOpenAI}, + info := &convmeta.Values{ + ConversionChain: []types.RelayFormat{types.RelayFormatOpenAI}, } req := &dto.GeneralOpenAIRequest{ Model: "gpt-test", @@ -133,13 +130,12 @@ func TestConvertRequestToTargetRecordsConversionChain(t *testing.T) { To: types.RelayFormatOpenAIResponses, }, }, result.Steps) - assert.Equal(t, []types.RelayFormat{types.RelayFormatOpenAI, types.RelayFormatOpenAIResponses}, info.RequestConversionChain) + assert.Equal(t, []types.RelayFormat{types.RelayFormatOpenAI, types.RelayFormatOpenAIResponses}, info.ConversionChain) } func TestConvertRequestPlansMultiHopPath(t *testing.T) { - info := &relaycommon.RelayInfo{ - RelayFormat: types.RelayFormatClaude, - RequestConversionChain: []types.RelayFormat{types.RelayFormatClaude}, + info := &convmeta.Values{ + ConversionChain: []types.RelayFormat{types.RelayFormatClaude}, } req := &dto.ClaudeRequest{ Model: "claude-test", @@ -168,13 +164,12 @@ func TestConvertRequestPlansMultiHopPath(t *testing.T) { To: types.RelayFormatOpenAIResponses, }, }, result.Steps) - assert.Equal(t, []types.RelayFormat{types.RelayFormatClaude, types.RelayFormatOpenAI, types.RelayFormatOpenAIResponses}, info.RequestConversionChain) + assert.Equal(t, []types.RelayFormat{types.RelayFormatClaude, types.RelayFormatOpenAI, types.RelayFormatOpenAIResponses}, info.ConversionChain) } func TestConvertRequestViaExecutesExplicitPath(t *testing.T) { - info := &relaycommon.RelayInfo{ - RelayFormat: types.RelayFormatOpenAI, - RequestConversionChain: []types.RelayFormat{types.RelayFormatOpenAI}, + info := &convmeta.Values{ + ConversionChain: []types.RelayFormat{types.RelayFormatOpenAI}, } req := &dto.GeneralOpenAIRequest{ Model: "gpt-test", @@ -194,16 +189,14 @@ func TestConvertRequestViaExecutesExplicitPath(t *testing.T) { To: types.RelayFormatOpenAIResponses, }, }, result.Steps) - assert.Equal(t, []types.RelayFormat{types.RelayFormatOpenAI, types.RelayFormatOpenAIResponses}, info.RequestConversionChain) + assert.Equal(t, []types.RelayFormat{types.RelayFormatOpenAI, types.RelayFormatOpenAIResponses}, info.ConversionChain) } func TestConvertRequestResponsesToGeminiAppliesResponsesPreprocess(t *testing.T) { - info := &relaycommon.RelayInfo{ - RelayFormat: types.RelayFormatOpenAIResponses, - RequestConversionChain: []types.RelayFormat{types.RelayFormatOpenAIResponses}, - ChannelMeta: &relaycommon.ChannelMeta{ - UpstreamModelName: "gemini-test", - }, + info := &convmeta.Values{ + ConversionChain: []types.RelayFormat{types.RelayFormatOpenAIResponses}, + ChannelMetaAttached: true, + UpstreamModelName: "gemini-test", } req := &dto.OpenAIResponsesRequest{ Model: "gemini-test", @@ -253,23 +246,15 @@ func TestConvertRequestResponsesToGeminiAppliesResponsesPreprocess(t *testing.T) To: types.RelayFormatGemini, }, }, result.Steps) - assert.Equal(t, []types.RelayFormat{types.RelayFormatOpenAIResponses, types.RelayFormatGemini}, info.RequestConversionChain) + assert.Equal(t, []types.RelayFormat{types.RelayFormatOpenAIResponses, types.RelayFormatGemini}, info.ConversionChain) } func TestConvertRequestResponsesToGeminiUsesDirectConverter(t *testing.T) { - geminiSettings := model_setting.GetGeminiSettings() - originalThoughtSignatureEnabled := geminiSettings.FunctionCallThoughtSignatureEnabled - geminiSettings.FunctionCallThoughtSignatureEnabled = true - t.Cleanup(func() { - geminiSettings.FunctionCallThoughtSignatureEnabled = originalThoughtSignatureEnabled - }) - - info := &relaycommon.RelayInfo{ - RelayFormat: types.RelayFormatOpenAIResponses, - RequestConversionChain: []types.RelayFormat{types.RelayFormatOpenAIResponses}, - ChannelMeta: &relaycommon.ChannelMeta{ - UpstreamModelName: "gemini-test", - }, + info := &convmeta.Values{ + Options: &convmeta.Options{Gemini: convmeta.GeminiOptions{FunctionCallThoughtSignatureEnabled: true}}, + ConversionChain: []types.RelayFormat{types.RelayFormatOpenAIResponses}, + ChannelMetaAttached: true, + UpstreamModelName: "gemini-test", } maxOutputTokens := uint(256) req := &dto.OpenAIResponsesRequest{ @@ -345,7 +330,7 @@ func TestConvertRequestResponsesToGeminiUsesDirectConverter(t *testing.T) { To: types.RelayFormatGemini, }, }, result.Steps) - assert.Equal(t, []types.RelayFormat{types.RelayFormatOpenAIResponses, types.RelayFormatGemini}, info.RequestConversionChain) + assert.Equal(t, []types.RelayFormat{types.RelayFormatOpenAIResponses, types.RelayFormatGemini}, info.ConversionChain) require.NotNil(t, geminiReq.SystemInstructions) require.Len(t, geminiReq.SystemInstructions.Parts, 1) @@ -355,7 +340,7 @@ func TestConvertRequestResponsesToGeminiUsesDirectConverter(t *testing.T) { tools := geminiReq.GetTools() require.Len(t, tools, 1) - functions, err := common.Any2Type[[]dto.FunctionRequest](tools[0].FunctionDeclarations) + functions, err := kitutil.Any2Type[[]dto.FunctionRequest](tools[0].FunctionDeclarations) require.NoError(t, err) require.Len(t, functions, 1) assert.Equal(t, "lookup", functions[0].Name) @@ -384,7 +369,7 @@ func TestConvertRequestResponsesToGeminiUsesDirectConverter(t *testing.T) { assert.Equal(t, "lookup", functionCall.FunctionName) assert.Equal(t, map[string]any{"q": "x"}, functionCall.Arguments) var thoughtSignature string - require.NoError(t, common.Unmarshal(geminiReq.Contents[0].Parts[0].ThoughtSignature, &thoughtSignature)) + require.NoError(t, kitutil.Unmarshal(geminiReq.Contents[0].Parts[0].ThoughtSignature, &thoughtSignature)) assert.Equal(t, sharedgemini.ThoughtSignatureBypassValue, thoughtSignature) assert.Equal(t, "I will call.", geminiReq.Contents[0].Parts[1].Text) @@ -398,19 +383,11 @@ func TestConvertRequestResponsesToGeminiUsesDirectConverter(t *testing.T) { } func TestConvertRequestResponsesToGeminiSkipsThoughtSignatureWhenDisabled(t *testing.T) { - geminiSettings := model_setting.GetGeminiSettings() - originalThoughtSignatureEnabled := geminiSettings.FunctionCallThoughtSignatureEnabled - geminiSettings.FunctionCallThoughtSignatureEnabled = false - t.Cleanup(func() { - geminiSettings.FunctionCallThoughtSignatureEnabled = originalThoughtSignatureEnabled - }) - - info := &relaycommon.RelayInfo{ - RelayFormat: types.RelayFormatOpenAIResponses, - RequestConversionChain: []types.RelayFormat{types.RelayFormatOpenAIResponses}, - ChannelMeta: &relaycommon.ChannelMeta{ - UpstreamModelName: "gemini-test", - }, + info := &convmeta.Values{ + Options: &convmeta.Options{Gemini: convmeta.GeminiOptions{FunctionCallThoughtSignatureEnabled: false}}, + ConversionChain: []types.RelayFormat{types.RelayFormatOpenAIResponses}, + ChannelMetaAttached: true, + UpstreamModelName: "gemini-test", } req := &dto.OpenAIResponsesRequest{ Model: "gemini-test", @@ -439,13 +416,6 @@ func TestConvertRequestResponsesToGeminiSkipsThoughtSignatureWhenDisabled(t *tes } func TestConvertRequestOpenAIChatToGeminiAddsThoughtSignatureForAdvancedCustom(t *testing.T) { - geminiSettings := model_setting.GetGeminiSettings() - originalThoughtSignatureEnabled := geminiSettings.FunctionCallThoughtSignatureEnabled - geminiSettings.FunctionCallThoughtSignatureEnabled = true - t.Cleanup(func() { - geminiSettings.FunctionCallThoughtSignatureEnabled = originalThoughtSignatureEnabled - }) - assistantMessage := dto.Message{Role: "assistant", Content: ""} assistantMessage.SetToolCalls([]dto.ToolCallRequest{ { @@ -457,13 +427,12 @@ func TestConvertRequestOpenAIChatToGeminiAddsThoughtSignatureForAdvancedCustom(t }, }, }) - info := &relaycommon.RelayInfo{ - RelayFormat: types.RelayFormatOpenAI, - RequestConversionChain: []types.RelayFormat{types.RelayFormatOpenAI}, - ChannelMeta: &relaycommon.ChannelMeta{ - ChannelType: constant.ChannelTypeAdvancedCustom, - UpstreamModelName: "gemini-test", - }, + info := &convmeta.Values{ + Options: &convmeta.Options{Gemini: convmeta.GeminiOptions{FunctionCallThoughtSignatureEnabled: true}}, + ConversionChain: []types.RelayFormat{types.RelayFormatOpenAI}, + ChannelMetaAttached: true, + ChannelType: 58, // advanced-custom in the host + UpstreamModelName: "gemini-test", } req := &dto.GeneralOpenAIRequest{ Model: "gemini-test", @@ -493,14 +462,13 @@ func TestConvertRequestOpenAIChatToGeminiAddsThoughtSignatureForAdvancedCustom(t require.Len(t, geminiReq.Contents[1].Parts, 1) require.NotNil(t, geminiReq.Contents[1].Parts[0].FunctionCall) var thoughtSignature string - require.NoError(t, common.Unmarshal(geminiReq.Contents[1].Parts[0].ThoughtSignature, &thoughtSignature)) + require.NoError(t, kitutil.Unmarshal(geminiReq.Contents[1].Parts[0].ThoughtSignature, &thoughtSignature)) assert.Equal(t, sharedgemini.ThoughtSignatureBypassValue, thoughtSignature) } func TestConvertRequestResponsesToClaudeUsesDirectConverter(t *testing.T) { - info := &relaycommon.RelayInfo{ - RelayFormat: types.RelayFormatOpenAIResponses, - RequestConversionChain: []types.RelayFormat{types.RelayFormatOpenAIResponses}, + info := &convmeta.Values{ + ConversionChain: []types.RelayFormat{types.RelayFormatOpenAIResponses}, } stream := true parallelToolCalls := false @@ -563,9 +531,9 @@ func TestConvertRequestResponsesToClaudeUsesDirectConverter(t *testing.T) { To: types.RelayFormatClaude, }, }, result.Steps) - assert.Equal(t, []types.RelayFormat{types.RelayFormatOpenAIResponses, types.RelayFormatClaude}, info.RequestConversionChain) + assert.Equal(t, []types.RelayFormat{types.RelayFormatOpenAIResponses, types.RelayFormatClaude}, info.ConversionChain) - system, err := common.Any2Type[[]dto.ClaudeMediaMessage](claudeReq.System) + system, err := kitutil.Any2Type[[]dto.ClaudeMediaMessage](claudeReq.System) require.NoError(t, err) require.Len(t, system, 1) assert.Equal(t, "system rules", system[0].GetText()) @@ -576,7 +544,7 @@ func TestConvertRequestResponsesToClaudeUsesDirectConverter(t *testing.T) { assert.Equal(t, "enabled", claudeReq.Thinking.Type) assert.Equal(t, 2048, claudeReq.Thinking.GetBudgetTokens()) - tools, err := common.Any2Type[[]*dto.Tool](claudeReq.Tools) + tools, err := kitutil.Any2Type[[]*dto.Tool](claudeReq.Tools) require.NoError(t, err) require.Len(t, tools, 1) assert.Equal(t, "lookup", tools[0].Name) @@ -608,12 +576,10 @@ func TestConvertRequestResponsesToClaudeUsesDirectConverter(t *testing.T) { } func TestConvertRequestViaResponsesToGeminiStillUsesDirectSteps(t *testing.T) { - info := &relaycommon.RelayInfo{ - RelayFormat: types.RelayFormatOpenAIResponses, - RequestConversionChain: []types.RelayFormat{types.RelayFormatOpenAIResponses}, - ChannelMeta: &relaycommon.ChannelMeta{ - UpstreamModelName: "gemini-test", - }, + info := &convmeta.Values{ + ConversionChain: []types.RelayFormat{types.RelayFormatOpenAIResponses}, + ChannelMetaAttached: true, + UpstreamModelName: "gemini-test", } req := &dto.OpenAIResponsesRequest{ Model: "gemini-test", @@ -645,9 +611,8 @@ func TestConvertRequestViaResponsesToGeminiStillUsesDirectSteps(t *testing.T) { } func TestConvertRequestByIDDeduplicatesConversionChain(t *testing.T) { - info := &relaycommon.RelayInfo{ - RelayFormat: types.RelayFormatOpenAI, - RequestConversionChain: []types.RelayFormat{types.RelayFormatOpenAI, types.RelayFormatOpenAIResponses}, + info := &convmeta.Values{ + ConversionChain: []types.RelayFormat{types.RelayFormatOpenAI, types.RelayFormatOpenAIResponses}, } req := &dto.GeneralOpenAIRequest{ Model: "gpt-test", @@ -661,13 +626,12 @@ func TestConvertRequestByIDDeduplicatesConversionChain(t *testing.T) { require.NoError(t, err) require.IsType(t, &dto.OpenAIResponsesRequest{}, result.Value) require.Len(t, result.Steps, 1) - assert.Equal(t, []types.RelayFormat{types.RelayFormatOpenAI, types.RelayFormatOpenAIResponses}, info.RequestConversionChain) + assert.Equal(t, []types.RelayFormat{types.RelayFormatOpenAI, types.RelayFormatOpenAIResponses}, info.ConversionChain) } func TestConvertRequestByIDExecutesMultiHopConverter(t *testing.T) { - info := &relaycommon.RelayInfo{ - RelayFormat: types.RelayFormatClaude, - RequestConversionChain: []types.RelayFormat{types.RelayFormatClaude}, + info := &convmeta.Values{ + ConversionChain: []types.RelayFormat{types.RelayFormatClaude}, } req := &dto.ClaudeRequest{ Model: "claude-test", @@ -694,15 +658,15 @@ func TestConvertRequestByIDExecutesMultiHopConverter(t *testing.T) { To: types.RelayFormatOpenAIResponses, }, }, result.Steps) - assert.Equal(t, []types.RelayFormat{types.RelayFormatClaude, types.RelayFormatOpenAI, types.RelayFormatOpenAIResponses}, info.RequestConversionChain) + assert.Equal(t, []types.RelayFormat{types.RelayFormatClaude, types.RelayFormatOpenAI, types.RelayFormatOpenAIResponses}, info.ConversionChain) } func TestConvertRequestRejectsUnsupportedConverterAndNilRequest(t *testing.T) { - _, err := ConvertRequestByID(nil, &relaycommon.RelayInfo{}, "missing_converter", &dto.GeneralOpenAIRequest{Model: "gpt-test"}) + _, err := ConvertRequestByID(nil, &convmeta.Values{}, "missing_converter", &dto.GeneralOpenAIRequest{Model: "gpt-test"}) require.Error(t, err) assert.Contains(t, err.Error(), "not registered") - _, err = ConvertRequest(nil, &relaycommon.RelayInfo{}, types.RelayFormatOpenAIResponses, (*dto.GeneralOpenAIRequest)(nil)) + _, err = ConvertRequest(nil, &convmeta.Values{}, types.RelayFormatOpenAIResponses, (*dto.GeneralOpenAIRequest)(nil)) require.Error(t, err) assert.Contains(t, err.Error(), "request is nil") } @@ -710,7 +674,7 @@ func TestConvertRequestRejectsUnsupportedConverterAndNilRequest(t *testing.T) { func TestConvertRequestByIDRejectsWrongSourceFormat(t *testing.T) { _, err := ConvertRequestByID( nil, - &relaycommon.RelayInfo{}, + &convmeta.Values{}, ConverterOpenAIChatToOpenAIResponses, &dto.ClaudeRequest{Model: "claude-test"}, ) @@ -722,7 +686,7 @@ func TestConvertRequestByIDRejectsWrongSourceFormat(t *testing.T) { func TestConvertRequestRejectsUnregisteredExplicitPath(t *testing.T) { _, err := ConvertRequest( nil, - &relaycommon.RelayInfo{}, + &convmeta.Values{}, types.RelayFormatEmbedding, &dto.ClaudeRequest{Model: "claude-test"}, ) @@ -733,7 +697,7 @@ func TestConvertRequestRejectsUnregisteredExplicitPath(t *testing.T) { func mustRawMessage(t *testing.T, value any) []byte { t.Helper() - raw, err := common.Marshal(value) + raw, err := kitutil.Marshal(value) require.NoError(t, err) return raw } diff --git a/service/relayconvert/response_compat.go b/relaykit/relayconvert/response_compat.go similarity index 88% rename from service/relayconvert/response_compat.go rename to relaykit/relayconvert/response_compat.go index b42bed42..68c57cd9 100644 --- a/service/relayconvert/response_compat.go +++ b/relaykit/relayconvert/response_compat.go @@ -1,12 +1,12 @@ package relayconvert import ( - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" - claudemessages "github.com/QuantumNous/new-api/service/relayconvert/internal/claude_messages" - geminichat "github.com/QuantumNous/new-api/service/relayconvert/internal/gemini_chat" - oaichat "github.com/QuantumNous/new-api/service/relayconvert/internal/oai_chat" - oairesponses "github.com/QuantumNous/new-api/service/relayconvert/internal/oai_responses" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + claudemessages "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/claude_messages" + geminichat "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/gemini_chat" + oaichat "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/oai_chat" + oairesponses "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/oai_responses" ) type ClaudeResponseInfo = claudemessages.ClaudeResponseInfo @@ -20,11 +20,11 @@ func NormalizeCacheCreationSplit(totalTokens int, tokens5m int, tokens1h int) (i return oaichat.NormalizeCacheCreationSplit(totalTokens, tokens5m, tokens1h) } -func ResponseOpenAI2Claude(openAIResponse *dto.OpenAITextResponse, info *relaycommon.RelayInfo) *dto.ClaudeResponse { +func ResponseOpenAI2Claude(openAIResponse *dto.OpenAITextResponse, info convmeta.Meta) *dto.ClaudeResponse { return oaichat.ResponseOpenAI2Claude(openAIResponse, info) } -func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamResponse, info *relaycommon.RelayInfo) []*dto.ClaudeResponse { +func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamResponse, info convmeta.Meta) []*dto.ClaudeResponse { return oaichat.StreamResponseOpenAI2Claude(openAIResponse, info) } @@ -60,11 +60,11 @@ func FormatClaudeResponseInfo(claudeResponse *dto.ClaudeResponse, oaiResponse *d return claudemessages.FormatClaudeResponseInfo(claudeResponse, oaiResponse, claudeInfo) } -func ResponseOpenAI2Gemini(openAIResponse *dto.OpenAITextResponse, info *relaycommon.RelayInfo) *dto.GeminiChatResponse { +func ResponseOpenAI2Gemini(openAIResponse *dto.OpenAITextResponse, info convmeta.Meta) *dto.GeminiChatResponse { return oaichat.ResponseOpenAI2Gemini(openAIResponse, info) } -func StreamResponseOpenAI2Gemini(openAIResponse *dto.ChatCompletionsStreamResponse, info *relaycommon.RelayInfo) *dto.GeminiChatResponse { +func StreamResponseOpenAI2Gemini(openAIResponse *dto.ChatCompletionsStreamResponse, info convmeta.Meta) *dto.GeminiChatResponse { return oaichat.StreamResponseOpenAI2Gemini(openAIResponse, info) } diff --git a/service/relayconvert/response_registry.go b/relaykit/relayconvert/response_registry.go similarity index 84% rename from service/relayconvert/response_registry.go rename to relaykit/relayconvert/response_registry.go index 43ef9d38..66326b36 100644 --- a/service/relayconvert/response_registry.go +++ b/relaykit/relayconvert/response_registry.go @@ -7,22 +7,22 @@ import ( "strings" "sync" - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" - "github.com/gin-gonic/gin" + "context" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" + "github.com/QuantumNous/new-api/relaykit/types" ) -type ResponseConverterFunc func(c *gin.Context, info *relaycommon.RelayInfo, response any) (any, *dto.Usage, error) +type ResponseConverterFunc func(c context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error) -type ResponseStreamConverterFunc func(c *gin.Context, info *relaycommon.RelayInfo, response any) (any, *dto.Usage, error) +type ResponseStreamConverterFunc func(c context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error) type ResponseStreamStateFactory func(options ResponseStreamOptions) any -type ResponseStreamChunkConverterFunc func(c *gin.Context, info *relaycommon.RelayInfo, response any, state any) ([]any, *dto.Usage, error) +type ResponseStreamChunkConverterFunc func(c context.Context, info convmeta.Meta, response any, state any) ([]any, *dto.Usage, error) -type ResponseStreamFinalizerFunc func(c *gin.Context, info *relaycommon.RelayInfo, state any) ([]any, *dto.Usage, error) +type ResponseStreamFinalizerFunc func(c context.Context, info convmeta.Meta, state any) ([]any, *dto.Usage, error) type ResponseConverterQuality string @@ -201,7 +201,7 @@ func LookupResponseConverter(converter string) (ResponseConverterSpec, bool) { return cloneResponseConverterSpec(spec), true } -func ConvertResponse(c *gin.Context, info *relaycommon.RelayInfo, target types.RelayFormat, response any) (*ResponseResult, error) { +func ConvertResponse(c context.Context, info convmeta.Meta, target types.RelayFormat, response any) (*ResponseResult, error) { from, err := inferResponseRelayFormat(response) if err != nil { return nil, err @@ -226,7 +226,7 @@ func ConvertResponse(c *gin.Context, info *relaycommon.RelayInfo, target types.R return executeResponseSpec(c, info, from, target, response, spec) } -func ConvertResponseByID(c *gin.Context, info *relaycommon.RelayInfo, converter string, response any) (*ResponseResult, error) { +func ConvertResponseByID(c context.Context, info convmeta.Meta, converter string, response any) (*ResponseResult, error) { from, err := inferResponseRelayFormat(response) if err != nil { return nil, err @@ -242,7 +242,7 @@ func ConvertResponseByID(c *gin.Context, info *relaycommon.RelayInfo, converter return executeResponseSpec(c, info, from, spec.To, response, spec) } -func ConvertStreamResponse(c *gin.Context, info *relaycommon.RelayInfo, target types.RelayFormat, response any) (*ResponseResult, error) { +func ConvertStreamResponse(c context.Context, info convmeta.Meta, target types.RelayFormat, response any) (*ResponseResult, error) { from, err := inferResponseRelayFormat(response) if err != nil { return nil, err @@ -296,7 +296,7 @@ func NewResponseStreamStateByID(converter string, options ResponseStreamOptions) return newResponseStreamStateFromSpec(spec.From, spec.To, options, spec) } -func ConvertStreamResponseChunk(c *gin.Context, info *relaycommon.RelayInfo, state *ResponseStreamState, response any) ([]ResponseResult, error) { +func ConvertStreamResponseChunk(c context.Context, info convmeta.Meta, state *ResponseStreamState, response any) ([]ResponseResult, error) { if state == nil { return nil, errors.New("response stream state is required") } @@ -321,7 +321,7 @@ func ConvertStreamResponseChunk(c *gin.Context, info *relaycommon.RelayInfo, sta return responseStreamResults(state, values, usage), nil } -func FinalizeStreamResponse(c *gin.Context, info *relaycommon.RelayInfo, state *ResponseStreamState) ([]ResponseResult, error) { +func FinalizeStreamResponse(c context.Context, info convmeta.Meta, state *ResponseStreamState) ([]ResponseResult, error) { if state == nil { return nil, errors.New("response stream state is required") } @@ -408,7 +408,7 @@ func (s *ResponseStreamState) UsageText() string { return "" } -func executeResponseSpec(c *gin.Context, info *relaycommon.RelayInfo, from types.RelayFormat, target types.RelayFormat, response any, spec ResponseConverterSpec) (*ResponseResult, error) { +func executeResponseSpec(c context.Context, info convmeta.Meta, from types.RelayFormat, target types.RelayFormat, response any, spec ResponseConverterSpec) (*ResponseResult, error) { steps, err := expandResponseConverterSteps(spec) if err != nil { return nil, err @@ -416,7 +416,7 @@ func executeResponseSpec(c *gin.Context, info *relaycommon.RelayInfo, from types return executeResponseSteps(c, info, from, target, response, spec.ID, spec.Quality, steps) } -func executeResponseSteps(c *gin.Context, info *relaycommon.RelayInfo, from types.RelayFormat, target types.RelayFormat, response any, converter string, quality ResponseConverterQuality, specs []ResponseConverterSpec) (*ResponseResult, error) { +func executeResponseSteps(c context.Context, info convmeta.Meta, from types.RelayFormat, target types.RelayFormat, response any, converter string, quality ResponseConverterQuality, specs []ResponseConverterSpec) (*ResponseResult, error) { current := response var usage *dto.Usage steps := make([]ResponseStep, 0, len(specs)) @@ -449,7 +449,7 @@ func executeResponseSteps(c *gin.Context, info *relaycommon.RelayInfo, from type }, nil } -func executeResponseStep(c *gin.Context, info *relaycommon.RelayInfo, spec ResponseConverterSpec, response any) (any, *dto.Usage, ResponseStep, error) { +func executeResponseStep(c context.Context, info convmeta.Meta, spec ResponseConverterSpec, response any) (any, *dto.Usage, ResponseStep, error) { if spec.Convert == nil { return nil, nil, ResponseStep{}, fmt.Errorf("response converter %q has no non-stream implementation", spec.ID) } @@ -465,7 +465,7 @@ func executeResponseStep(c *gin.Context, info *relaycommon.RelayInfo, spec Respo }, nil } -func executeStatelessStreamResponseSpec(c *gin.Context, info *relaycommon.RelayInfo, from types.RelayFormat, target types.RelayFormat, response any, spec ResponseConverterSpec) (*ResponseResult, error) { +func executeStatelessStreamResponseSpec(c context.Context, info convmeta.Meta, from types.RelayFormat, target types.RelayFormat, response any, spec ResponseConverterSpec) (*ResponseResult, error) { steps, err := expandResponseConverterSteps(spec) if err != nil { return nil, err @@ -531,7 +531,7 @@ func newResponseStreamStateFromSpec(from types.RelayFormat, target types.RelayFo }, nil } -func executeResponseStreamSteps(c *gin.Context, info *relaycommon.RelayInfo, state *ResponseStreamState, values []any, start int) ([]any, *dto.Usage, error) { +func executeResponseStreamSteps(c context.Context, info convmeta.Meta, state *ResponseStreamState, values []any, start int) ([]any, *dto.Usage, error) { current := values var usage *dto.Usage for i := start; i < len(state.specs); i++ { @@ -557,7 +557,7 @@ func executeResponseStreamSteps(c *gin.Context, info *relaycommon.RelayInfo, sta return current, usage, nil } -func prepareResponseStreamInfo(info *relaycommon.RelayInfo, spec ResponseConverterSpec) { +func prepareResponseStreamInfo(info convmeta.Meta, spec ResponseConverterSpec) { if info == nil { return } @@ -567,10 +567,10 @@ func prepareResponseStreamInfo(info *relaycommon.RelayInfo, spec ResponseConvert if spec.To != types.RelayFormatClaude && spec.To != types.RelayFormatGemini { return } - info.SendResponseCount++ + info.IncrSendResponseCount() } -func executeResponseStreamStep(c *gin.Context, info *relaycommon.RelayInfo, spec ResponseConverterSpec, state any, response any) ([]any, *dto.Usage, error) { +func executeResponseStreamStep(c context.Context, info convmeta.Meta, spec ResponseConverterSpec, state any, response any) ([]any, *dto.Usage, error) { if spec.ConvertStreamChunk != nil { return spec.ConvertStreamChunk(c, info, response, state) } @@ -584,7 +584,7 @@ func executeResponseStreamStep(c *gin.Context, info *relaycommon.RelayInfo, spec return streamValuesFromAny(value), usage, nil } -func finalizeResponseStreamStep(c *gin.Context, info *relaycommon.RelayInfo, spec ResponseConverterSpec, state any) ([]any, *dto.Usage, error) { +func finalizeResponseStreamStep(c context.Context, info convmeta.Meta, spec ResponseConverterSpec, state any) ([]any, *dto.Usage, error) { if spec.FinalizeStream == nil { return nil, nil, nil } @@ -786,26 +786,26 @@ func usageFromClaudeResponse(resp *dto.ClaudeResponse) *dto.Usage { return nil } -func convertOAIChatResponseToOAIResponses(_ *gin.Context, _ *relaycommon.RelayInfo, response any) (any, *dto.Usage, error) { +func convertOAIChatResponseToOAIResponses(_ context.Context, _ convmeta.Meta, response any) (any, *dto.Usage, error) { chatResponse, err := asOAIChatResponse(response) if err != nil { return nil, nil, err } id := strings.TrimSpace(chatResponse.Id) if id == "" { - id = fmt.Sprintf("resp_%s", common.GetUUID()) + id = fmt.Sprintf("resp_%s", kitutil.GetUUID()) } return ChatCompletionsResponseToResponsesResponse(chatResponse, id) } -func convertOAIResponsesResponseToOAIChat(_ *gin.Context, _ *relaycommon.RelayInfo, response any) (any, *dto.Usage, error) { +func convertOAIResponsesResponseToOAIChat(_ context.Context, _ convmeta.Meta, response any) (any, *dto.Usage, error) { responsesResponse, err := asOAIResponsesResponse(response) if err != nil { return nil, nil, err } id := strings.TrimSpace(responsesResponse.ID) if id == "" { - id = fmt.Sprintf("chatcmpl-%s", common.GetUUID()) + id = fmt.Sprintf("chatcmpl-%s", kitutil.GetUUID()) } return ResponsesResponseToChatCompletionsResponse(responsesResponse, id) } @@ -813,7 +813,7 @@ func convertOAIResponsesResponseToOAIChat(_ *gin.Context, _ *relaycommon.RelayIn func newOAIChatToOAIResponsesStreamState(options ResponseStreamOptions) any { id := strings.TrimSpace(options.ID) if id == "" { - id = fmt.Sprintf("resp_%s", common.GetUUID()) + id = fmt.Sprintf("resp_%s", kitutil.GetUUID()) } state := NewChatToResponsesStreamState(id, strings.TrimSpace(options.Model)) if options.Created != 0 { @@ -822,7 +822,7 @@ func newOAIChatToOAIResponsesStreamState(options ResponseStreamOptions) any { return state } -func convertOAIChatStreamResponseToOAIResponses(_ *gin.Context, _ *relaycommon.RelayInfo, response any, state any) ([]any, *dto.Usage, error) { +func convertOAIChatStreamResponseToOAIResponses(_ context.Context, _ convmeta.Meta, response any, state any) ([]any, *dto.Usage, error) { chatResponse, err := asOAIChatStreamResponse(response) if err != nil { return nil, nil, err @@ -838,7 +838,7 @@ func convertOAIChatStreamResponseToOAIResponses(_ *gin.Context, _ *relaycommon.R return streamValuesFromAny(events), streamState.Usage, nil } -func finalizeOAIChatStreamResponseToOAIResponses(_ *gin.Context, _ *relaycommon.RelayInfo, state any) ([]any, *dto.Usage, error) { +func finalizeOAIChatStreamResponseToOAIResponses(_ context.Context, _ convmeta.Meta, state any) ([]any, *dto.Usage, error) { streamState, ok := state.(*ChatToResponsesStreamState) if !ok || streamState == nil { return nil, nil, errors.New("OAI chat to OAI responses stream state is required") @@ -856,7 +856,7 @@ func newOAIResponsesToOAIChatStreamState(options ResponseStreamOptions) any { return state } -func convertOAIResponsesStreamResponseToOAIChat(_ *gin.Context, _ *relaycommon.RelayInfo, response any, state any) ([]any, *dto.Usage, error) { +func convertOAIResponsesStreamResponseToOAIChat(_ context.Context, _ convmeta.Meta, response any, state any) ([]any, *dto.Usage, error) { responsesResponse, err := asOAIResponsesStreamResponse(response) if err != nil { return nil, nil, err @@ -872,7 +872,7 @@ func convertOAIResponsesStreamResponseToOAIChat(_ *gin.Context, _ *relaycommon.R return streamValuesFromAny(chunks), streamState.Usage, nil } -func finalizeOAIResponsesStreamResponseToOAIChat(_ *gin.Context, _ *relaycommon.RelayInfo, state any) ([]any, *dto.Usage, error) { +func finalizeOAIResponsesStreamResponseToOAIChat(_ context.Context, _ convmeta.Meta, state any) ([]any, *dto.Usage, error) { streamState, ok := state.(*ResponsesToChatStreamState) if !ok || streamState == nil { return nil, nil, errors.New("OAI responses to OAI chat stream state is required") @@ -881,7 +881,7 @@ func finalizeOAIResponsesStreamResponseToOAIChat(_ *gin.Context, _ *relaycommon. return streamValuesFromAny(chunks), streamState.Usage, nil } -func convertOAIChatResponseToClaudeMessages(_ *gin.Context, info *relaycommon.RelayInfo, response any) (any, *dto.Usage, error) { +func convertOAIChatResponseToClaudeMessages(_ context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error) { chatResponse, err := asOAIChatResponse(response) if err != nil { return nil, nil, err @@ -889,7 +889,7 @@ func convertOAIChatResponseToClaudeMessages(_ *gin.Context, info *relaycommon.Re return ResponseOpenAI2Claude(chatResponse, info), UsageFromChatUsage(&chatResponse.Usage), nil } -func convertOAIChatStreamResponseToClaudeMessages(_ *gin.Context, info *relaycommon.RelayInfo, response any) (any, *dto.Usage, error) { +func convertOAIChatStreamResponseToClaudeMessages(_ context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error) { chatResponse, err := asOAIChatStreamResponse(response) if err != nil { return nil, nil, err @@ -897,7 +897,7 @@ func convertOAIChatStreamResponseToClaudeMessages(_ *gin.Context, info *relaycom return StreamResponseOpenAI2Claude(chatResponse, info), canonicalUsageFromResponse(chatResponse), nil } -func convertOAIChatResponseToGeminiChat(_ *gin.Context, info *relaycommon.RelayInfo, response any) (any, *dto.Usage, error) { +func convertOAIChatResponseToGeminiChat(_ context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error) { chatResponse, err := asOAIChatResponse(response) if err != nil { return nil, nil, err @@ -905,7 +905,7 @@ func convertOAIChatResponseToGeminiChat(_ *gin.Context, info *relaycommon.RelayI return ResponseOpenAI2Gemini(chatResponse, info), UsageFromChatUsage(&chatResponse.Usage), nil } -func convertOAIChatStreamResponseToGeminiChat(_ *gin.Context, info *relaycommon.RelayInfo, response any) (any, *dto.Usage, error) { +func convertOAIChatStreamResponseToGeminiChat(_ context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error) { chatResponse, err := asOAIChatStreamResponse(response) if err != nil { return nil, nil, err @@ -913,7 +913,7 @@ func convertOAIChatStreamResponseToGeminiChat(_ *gin.Context, info *relaycommon. return StreamResponseOpenAI2Gemini(chatResponse, info), canonicalUsageFromResponse(chatResponse), nil } -func convertClaudeMessagesResponseToOAIChat(_ *gin.Context, _ *relaycommon.RelayInfo, response any) (any, *dto.Usage, error) { +func convertClaudeMessagesResponseToOAIChat(_ context.Context, _ convmeta.Meta, response any) (any, *dto.Usage, error) { claudeResponse, err := asClaudeResponse(response) if err != nil { return nil, nil, err @@ -926,7 +926,7 @@ func convertClaudeMessagesResponseToOAIChat(_ *gin.Context, _ *relaycommon.Relay return openAIResponse, usage, nil } -func convertClaudeMessagesStreamResponseToOAIChat(_ *gin.Context, _ *relaycommon.RelayInfo, response any) (any, *dto.Usage, error) { +func convertClaudeMessagesStreamResponseToOAIChat(_ context.Context, _ convmeta.Meta, response any) (any, *dto.Usage, error) { claudeResponse, err := asClaudeResponse(response) if err != nil { return nil, nil, err @@ -939,15 +939,15 @@ func convertClaudeMessagesStreamResponseToOAIChat(_ *gin.Context, _ *relaycommon return openAIResponse, usage, nil } -func convertGeminiChatResponseToOAIChat(_ *gin.Context, info *relaycommon.RelayInfo, response any) (any, *dto.Usage, error) { +func convertGeminiChatResponseToOAIChat(_ context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error) { geminiResponse, err := asGeminiChatResponse(response) if err != nil { return nil, nil, err } usage := UsageFromGeminiMetadata(geminiResponse.GetUsageMetadata(), fallbackPromptTokens(info)) - openAIResponse := ResponseGeminiChat2OpenAI(fmt.Sprintf("chatcmpl-%s", common.GetUUID()), common.GetTimestamp(), geminiResponse) - if info != nil && info.ChannelMeta != nil { - openAIResponse.Model = info.UpstreamModelName + openAIResponse := ResponseGeminiChat2OpenAI(fmt.Sprintf("chatcmpl-%s", kitutil.GetUUID()), kitutil.GetTimestamp(), geminiResponse) + if info != nil && info.HasChannelMeta() { + openAIResponse.Model = info.GetUpstreamModelName() } if usage != nil { openAIResponse.Usage = *usage @@ -955,7 +955,7 @@ func convertGeminiChatResponseToOAIChat(_ *gin.Context, info *relaycommon.RelayI return openAIResponse, usage, nil } -func convertGeminiChatStreamResponseToOAIChat(_ *gin.Context, info *relaycommon.RelayInfo, response any) (any, *dto.Usage, error) { +func convertGeminiChatStreamResponseToOAIChat(_ context.Context, info convmeta.Meta, response any) (any, *dto.Usage, error) { geminiResponse, err := asGeminiChatResponse(response) if err != nil { return nil, nil, err @@ -963,17 +963,17 @@ func convertGeminiChatStreamResponseToOAIChat(_ *gin.Context, info *relaycommon. openAIResponse, _ := StreamResponseGeminiChat2OpenAI(geminiResponse) usage := UsageFromGeminiMetadata(geminiResponse.GetUsageMetadata(), fallbackPromptTokens(info)) if openAIResponse != nil { - openAIResponse.Id = fmt.Sprintf("chatcmpl-%s", common.GetUUID()) - openAIResponse.Created = common.GetTimestamp() - if info != nil && info.ChannelMeta != nil { - openAIResponse.Model = info.UpstreamModelName + openAIResponse.Id = fmt.Sprintf("chatcmpl-%s", kitutil.GetUUID()) + openAIResponse.Created = kitutil.GetTimestamp() + if info != nil && info.HasChannelMeta() { + openAIResponse.Model = info.GetUpstreamModelName() } openAIResponse.Usage = usage } return openAIResponse, usage, nil } -func fallbackPromptTokens(info *relaycommon.RelayInfo) int { +func fallbackPromptTokens(info convmeta.Meta) int { if info == nil { return 0 } diff --git a/service/relayconvert/response_registry_test.go b/relaykit/relayconvert/response_registry_test.go similarity index 95% rename from service/relayconvert/response_registry_test.go rename to relaykit/relayconvert/response_registry_test.go index e5553cc0..3e62d4c2 100644 --- a/service/relayconvert/response_registry_test.go +++ b/relaykit/relayconvert/response_registry_test.go @@ -3,9 +3,9 @@ package relayconvert import ( "testing" - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -124,7 +124,7 @@ func TestConvertResponseRejectsNilAndUnsupportedRoute(t *testing.T) { func TestConvertResponseDirectConverters(t *testing.T) { chat := textRegistryChatResponse() - info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: "gemini-test"}} + info := &convmeta.Values{ChannelMetaAttached: true, UpstreamModelName: "gemini-test"} toResponses, err := ConvertResponse(nil, info, types.RelayFormatOpenAIResponses, chat) require.NoError(t, err) @@ -196,7 +196,7 @@ func TestConvertResponseDirectConverters(t *testing.T) { func TestConvertResponseMultiHopConverters(t *testing.T) { responses := textRegistryResponsesResponse() - toClaude, err := ConvertResponse(nil, &relaycommon.RelayInfo{}, types.RelayFormatClaude, responses) + toClaude, err := ConvertResponse(nil, &convmeta.Values{}, types.RelayFormatClaude, responses) require.NoError(t, err) assert.Equal(t, requestConverterResponsesToClaude, toClaude.Converter) assert.Equal(t, ResponseConverterQualityFair, toClaude.Quality) @@ -213,7 +213,7 @@ func TestConvertResponseMultiHopConverters(t *testing.T) { assert.Equal(t, map[string]interface{}{"q": "x"}, claudeValue.Content[1].Input) assert.Equal(t, 11, toClaude.Usage.TotalTokens) - toGemini, err := ConvertResponse(nil, &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: "gemini-test"}}, types.RelayFormatGemini, responses) + toGemini, err := ConvertResponse(nil, &convmeta.Values{ChannelMetaAttached: true, UpstreamModelName: "gemini-test"}, types.RelayFormatGemini, responses) require.NoError(t, err) assert.Equal(t, ConverterOpenAIResponsesToGemini, toGemini.Converter) assert.Equal(t, ResponseConverterQualityFair, toGemini.Quality) @@ -322,7 +322,7 @@ func TestConvertResponseProviderToOAIChatUsage(t *testing.T) { }, }, } - toChat, err = ConvertResponse(nil, &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: "gemini-test"}}, types.RelayFormatOpenAI, gemini) + toChat, err = ConvertResponse(nil, &convmeta.Values{ChannelMetaAttached: true, UpstreamModelName: "gemini-test"}, types.RelayFormatOpenAI, gemini) require.NoError(t, err) assert.Equal(t, ConverterGeminiContentToOpenAIChat, toChat.Converter) require.IsType(t, &dto.OpenAITextResponse{}, toChat.Value) @@ -405,9 +405,9 @@ func TestConvertResponseUsesBillingUsageWhenRestoringNativeTargets(t *testing.T) } func TestConvertStreamResponseDirectConverters(t *testing.T) { - info := &relaycommon.RelayInfo{ - ClaudeConvertInfo: &relaycommon.ClaudeConvertInfo{ - LastMessagesType: relaycommon.LastMessageTypeNone, + info := &convmeta.Values{ + ClaudeConvertInfo: &convmeta.ClaudeConvertInfo{ + LastMessagesType: convmeta.LastMessageTypeNone, }, } info.SendResponseCount = 1 @@ -431,7 +431,7 @@ func TestConvertStreamResponseDirectConverters(t *testing.T) { require.IsType(t, []*dto.ClaudeResponse{}, result.Value) assert.Equal(t, 5, result.Usage.TotalTokens) - result, err = ConvertStreamResponse(nil, &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: "gemini-test"}}, types.RelayFormatOpenAI, &dto.GeminiChatResponse{ + result, err = ConvertStreamResponse(nil, &convmeta.Values{ChannelMetaAttached: true, UpstreamModelName: "gemini-test"}, types.RelayFormatOpenAI, &dto.GeminiChatResponse{ Candidates: []dto.GeminiChatCandidate{{Content: dto.GeminiChatContent{Parts: []dto.GeminiPart{{Text: "hello"}}}}}, UsageMetadata: dto.GeminiUsageMetadata{ PromptTokenCount: 1, @@ -490,9 +490,9 @@ func TestConvertStreamResponseStatefulDirectConverters(t *testing.T) { } func TestConvertStreamResponseStatefulMultiHopResponsesToClaude(t *testing.T) { - info := &relaycommon.RelayInfo{ - ClaudeConvertInfo: &relaycommon.ClaudeConvertInfo{ - LastMessagesType: relaycommon.LastMessageTypeNone, + info := &convmeta.Values{ + ClaudeConvertInfo: &convmeta.ClaudeConvertInfo{ + LastMessagesType: convmeta.LastMessageTypeNone, }, } state, err := NewResponseStreamState(types.RelayFormatOpenAIResponses, types.RelayFormatClaude, ResponseStreamOptions{ diff --git a/relaykit/relayconvert/safety_settings_test.go b/relaykit/relayconvert/safety_settings_test.go new file mode 100644 index 00000000..ff2ec76e --- /dev/null +++ b/relaykit/relayconvert/safety_settings_test.go @@ -0,0 +1,94 @@ +package relayconvert + +import ( + "context" + "testing" + + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOpenAIToGeminiSafetySettings(t *testing.T) { + converters := []struct { + name string + convert func(t *testing.T, meta convmeta.Meta) *dto.GeminiChatRequest + }{ + { + name: "chat completions", + convert: func(t *testing.T, meta convmeta.Meta) *dto.GeminiChatRequest { + t.Helper() + got, err := OpenAIChatRequestToGeminiGenerateContent(context.Background(), dto.GeneralOpenAIRequest{ + Model: "gemini-test", + Messages: []dto.Message{ + {Role: "user", Content: "hello"}, + }, + }, meta) + require.NoError(t, err) + return got + }, + }, + { + name: "responses", + convert: func(t *testing.T, meta convmeta.Meta) *dto.GeminiChatRequest { + t.Helper() + got, err := OpenAIResponsesRequestToGeminiChat(context.Background(), &dto.OpenAIResponsesRequest{ + Model: "gemini-test", + Input: []byte(`"hello"`), + }, meta) + require.NoError(t, err) + return got + }, + }, + } + + for _, converter := range converters { + t.Run(converter.name, func(t *testing.T) { + t.Run("nil meta", func(t *testing.T) { + got := converter.convert(t, nil) + assert.Empty(t, got.SafetySettings) + body, err := kitutil.Marshal(got) + require.NoError(t, err) + assert.NotContains(t, string(body), `"safetySettings"`) + }) + + t.Run("zero options", func(t *testing.T) { + got := converter.convert(t, &convmeta.Values{}) + assert.Empty(t, got.SafetySettings) + body, err := kitutil.Marshal(got) + require.NoError(t, err) + assert.NotContains(t, string(body), `"safetySettings"`) + }) + + t.Run("empty thresholds", func(t *testing.T) { + got := converter.convert(t, &convmeta.Values{Options: &convmeta.Options{ + Gemini: convmeta.GeminiOptions{ + SafetySetting: func(category string) string { + if category == "HARM_CATEGORY_HARASSMENT" { + return "BLOCK_NONE" + } + return "" + }, + }, + }}) + assert.Equal(t, []dto.GeminiChatSafetySettings{ + {Category: "HARM_CATEGORY_HARASSMENT", Threshold: "BLOCK_NONE"}, + }, got.SafetySettings) + }) + + t.Run("nonempty thresholds", func(t *testing.T) { + got := converter.convert(t, &convmeta.Values{Options: &convmeta.Options{ + Gemini: convmeta.GeminiOptions{ + SafetySetting: func(string) string { return "OFF" }, + }, + }}) + require.Len(t, got.SafetySettings, 4) + for _, setting := range got.SafetySettings { + assert.Equal(t, "OFF", setting.Threshold) + } + }) + }) + } +} diff --git a/relaykit/relayconvert/testdata/golden/request/claude_to_gemini.golden.json b/relaykit/relayconvert/testdata/golden/request/claude_to_gemini.golden.json new file mode 100644 index 00000000..8ec074a6 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/request/claude_to_gemini.golden.json @@ -0,0 +1,94 @@ +{ + "contents": [ + { + "role": "user", + "parts": [ + { + "text": "What is in this image?" + }, + { + "inlineData": { + "mimeType": "image/png", + "data": "aGVsbG8=" + } + } + ] + }, + { + "role": "model", + "parts": [ + { + "functionCall": { + "name": "get_weather", + "args": { + "city": "Paris" + } + }, + "thoughtSignature": "context_engineering_is_the_way_to_go" + } + ] + }, + { + "role": "user", + "parts": [ + { + "functionResponse": { + "name": "get_weather", + "response": { + "content": "15 degrees" + } + } + } + ] + } + ], + "safetySettings": [ + { + "category": "HARM_CATEGORY_HARASSMENT", + "threshold": "OFF" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "threshold": "OFF" + }, + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "threshold": "OFF" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "threshold": "OFF" + } + ], + "generationConfig": { + "maxOutputTokens": 1024 + }, + "tools": [ + { + "functionDeclarations": [ + { + "description": "Get weather by city", + "name": "get_weather", + "parameters": { + "properties": { + "city": { + "type": "STRING" + } + }, + "required": [ + "city" + ], + "type": "OBJECT" + } + } + ] + } + ], + "systemInstruction": { + "parts": [ + { + "text": "You are a helpful assistant." + } + ] + } +} diff --git a/relaykit/relayconvert/testdata/golden/request/claude_to_openai.golden.json b/relaykit/relayconvert/testdata/golden/request/claude_to_openai.golden.json new file mode 100644 index 00000000..59a20e45 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/request/claude_to_openai.golden.json @@ -0,0 +1,67 @@ +{ + "model": "claude-test", + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What is in this image?" + }, + { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64,aGVsbG8=", + "MimeType": "" + } + } + ] + }, + { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "toolu_abc", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"city\":\"Paris\"}" + } + } + ] + }, + { + "role": "tool", + "content": "15 degrees", + "name": "get_weather", + "tool_call_id": "toolu_abc" + } + ], + "stream": true, + "max_tokens": 1024, + "tools": [ + { + "type": "function", + "function": { + "description": "Get weather by city", + "name": "get_weather", + "parameters": { + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "type": "object" + } + } + } + ] +} diff --git a/relaykit/relayconvert/testdata/golden/request/claude_to_openai_responses.golden.json b/relaykit/relayconvert/testdata/golden/request/claude_to_openai_responses.golden.json new file mode 100644 index 00000000..e5f68863 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/request/claude_to_openai_responses.golden.json @@ -0,0 +1,54 @@ +{ + "model": "claude-test", + "input": [ + { + "content": [ + { + "text": "What is in this image?", + "type": "input_text" + }, + { + "image_url": "data:image/png;base64,aGVsbG8=", + "type": "input_image" + } + ], + "role": "user" + }, + { + "content": "", + "role": "assistant" + }, + { + "arguments": "{\"city\":\"Paris\"}", + "call_id": "toolu_abc", + "name": "get_weather", + "type": "function_call" + }, + { + "call_id": "toolu_abc", + "output": "15 degrees", + "type": "function_call_output" + } + ], + "instructions": "You are a helpful assistant.", + "max_output_tokens": 1024, + "stream": true, + "tools": [ + { + "description": "Get weather by city", + "name": "get_weather", + "parameters": { + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "type": "object" + }, + "type": "function" + } + ] +} diff --git a/relaykit/relayconvert/testdata/golden/request/gemini_to_claude.golden.json b/relaykit/relayconvert/testdata/golden/request/gemini_to_claude.golden.json new file mode 100644 index 00000000..9539d2b5 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/request/gemini_to_claude.golden.json @@ -0,0 +1,61 @@ +{ + "model": "upstream-model", + "system": [ + { + "type": "text", + "text": "You are a helpful assistant." + } + ], + "messages": [ + { + "role": "user", + "content": [] + }, + { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "..." + }, + { + "type": "tool_use", + "id": "call_1", + "name": "get_weather", + "input": { + "city": "Paris" + } + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "tool_result", + "content": "{\"result\":\"15 degrees\"}", + "tool_use_id": "call_0" + } + ] + } + ], + "max_tokens": 1024, + "temperature": 0.7, + "tools": [ + { + "name": "get_weather", + "description": "Get weather by city", + "input_schema": { + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "type": "object" + } + } + ] +} diff --git a/relaykit/relayconvert/testdata/golden/request/gemini_to_openai.golden.json b/relaykit/relayconvert/testdata/golden/request/gemini_to_openai.golden.json new file mode 100644 index 00000000..bc1e2675 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/request/gemini_to_openai.golden.json @@ -0,0 +1,68 @@ +{ + "model": "upstream-model", + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What is in this image?" + }, + { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64,aGVsbG8=", + "detail": "auto", + "MimeType": "image/png" + } + } + ] + }, + { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"city\":\"Paris\"}" + } + } + ] + }, + { + "role": "tool", + "content": "{\"result\":\"15 degrees\"}", + "tool_call_id": "call_0" + } + ], + "stream": false, + "max_tokens": 1024, + "temperature": 0.7, + "tools": [ + { + "type": "function", + "function": { + "description": "Get weather by city", + "name": "get_weather", + "parameters": { + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "type": "object" + } + } + } + ] +} diff --git a/relaykit/relayconvert/testdata/golden/request/gemini_to_openai_responses.golden.json b/relaykit/relayconvert/testdata/golden/request/gemini_to_openai_responses.golden.json new file mode 100644 index 00000000..ec9ba42b --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/request/gemini_to_openai_responses.golden.json @@ -0,0 +1,55 @@ +{ + "model": "upstream-model", + "input": [ + { + "content": [ + { + "text": "What is in this image?", + "type": "input_text" + }, + { + "image_url": "data:image/png;base64,aGVsbG8=", + "type": "input_image" + } + ], + "role": "user" + }, + { + "content": "", + "role": "assistant" + }, + { + "arguments": "{\"city\":\"Paris\"}", + "call_id": "call_1", + "name": "get_weather", + "type": "function_call" + }, + { + "call_id": "call_0", + "output": "{\"result\":\"15 degrees\"}", + "type": "function_call_output" + } + ], + "instructions": "You are a helpful assistant.", + "max_output_tokens": 1024, + "stream": false, + "temperature": 0.7, + "tools": [ + { + "description": "Get weather by city", + "name": "get_weather", + "parameters": { + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "type": "object" + }, + "type": "function" + } + ] +} diff --git a/relaykit/relayconvert/testdata/golden/request/openai_responses_to_claude.golden.json b/relaykit/relayconvert/testdata/golden/request/openai_responses_to_claude.golden.json new file mode 100644 index 00000000..644eefb9 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/request/openai_responses_to_claude.golden.json @@ -0,0 +1,70 @@ +{ + "model": "gpt-test", + "system": [ + { + "type": "text", + "text": "You are a helpful assistant." + } + ], + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What is in this image?" + }, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": "aGVsbG8=" + } + } + ] + }, + { + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "call_abc", + "name": "get_weather", + "input": { + "city": "Paris" + } + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "tool_result", + "content": "15 degrees", + "tool_use_id": "call_abc" + } + ] + } + ], + "max_tokens": 1024, + "stream": true, + "tools": [ + { + "name": "get_weather", + "description": "Get weather by city", + "input_schema": { + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "type": "object" + } + } + ] +} diff --git a/relaykit/relayconvert/testdata/golden/request/openai_responses_to_gemini.golden.json b/relaykit/relayconvert/testdata/golden/request/openai_responses_to_gemini.golden.json new file mode 100644 index 00000000..8ec074a6 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/request/openai_responses_to_gemini.golden.json @@ -0,0 +1,94 @@ +{ + "contents": [ + { + "role": "user", + "parts": [ + { + "text": "What is in this image?" + }, + { + "inlineData": { + "mimeType": "image/png", + "data": "aGVsbG8=" + } + } + ] + }, + { + "role": "model", + "parts": [ + { + "functionCall": { + "name": "get_weather", + "args": { + "city": "Paris" + } + }, + "thoughtSignature": "context_engineering_is_the_way_to_go" + } + ] + }, + { + "role": "user", + "parts": [ + { + "functionResponse": { + "name": "get_weather", + "response": { + "content": "15 degrees" + } + } + } + ] + } + ], + "safetySettings": [ + { + "category": "HARM_CATEGORY_HARASSMENT", + "threshold": "OFF" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "threshold": "OFF" + }, + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "threshold": "OFF" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "threshold": "OFF" + } + ], + "generationConfig": { + "maxOutputTokens": 1024 + }, + "tools": [ + { + "functionDeclarations": [ + { + "description": "Get weather by city", + "name": "get_weather", + "parameters": { + "properties": { + "city": { + "type": "STRING" + } + }, + "required": [ + "city" + ], + "type": "OBJECT" + } + } + ] + } + ], + "systemInstruction": { + "parts": [ + { + "text": "You are a helpful assistant." + } + ] + } +} diff --git a/relaykit/relayconvert/testdata/golden/request/openai_responses_to_openai.golden.json b/relaykit/relayconvert/testdata/golden/request/openai_responses_to_openai.golden.json new file mode 100644 index 00000000..9f021f54 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/request/openai_responses_to_openai.golden.json @@ -0,0 +1,63 @@ +{ + "model": "gpt-test", + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": [ + { + "text": "What is in this image?", + "type": "text" + }, + { + "image_url": "https://example.com/cat.png", + "type": "image_url" + } + ] + }, + { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_abc", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"city\":\"Paris\"}" + } + } + ] + }, + { + "role": "tool", + "content": "15 degrees", + "tool_call_id": "call_abc" + } + ], + "stream": true, + "max_completion_tokens": 1024, + "tools": [ + { + "type": "function", + "function": { + "description": "Get weather by city", + "name": "get_weather", + "parameters": { + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "type": "object" + } + } + } + ] +} diff --git a/relaykit/relayconvert/testdata/golden/request/openai_to_claude.golden.json b/relaykit/relayconvert/testdata/golden/request/openai_to_claude.golden.json new file mode 100644 index 00000000..2bca2dd7 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/request/openai_to_claude.golden.json @@ -0,0 +1,81 @@ +{ + "model": "gpt-test", + "system": [ + { + "type": "text", + "text": "You are a helpful assistant." + } + ], + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What is in this image?" + }, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": "aGVsbG8=" + } + } + ] + }, + { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "..." + }, + { + "type": "tool_use", + "id": "call_abc", + "name": "get_weather", + "input": { + "city": "Paris" + } + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "tool_result", + "content": "15 degrees", + "tool_use_id": "call_abc" + } + ] + }, + { + "role": "user", + "content": "Summarize." + } + ], + "max_tokens": 1024, + "stream": true, + "tools": [ + { + "name": "get_weather", + "description": "Get weather by city", + "input_schema": { + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "type": "object" + } + } + ], + "tool_choice": { + "type": "auto" + } +} diff --git a/relaykit/relayconvert/testdata/golden/request/openai_to_gemini.golden.json b/relaykit/relayconvert/testdata/golden/request/openai_to_gemini.golden.json new file mode 100644 index 00000000..93b0ebb2 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/request/openai_to_gemini.golden.json @@ -0,0 +1,107 @@ +{ + "contents": [ + { + "role": "user", + "parts": [ + { + "text": "What is in this image?" + }, + { + "inlineData": { + "mimeType": "image/png", + "data": "aGVsbG8=" + } + } + ] + }, + { + "role": "model", + "parts": [ + { + "functionCall": { + "name": "get_weather", + "args": { + "city": "Paris" + } + }, + "thoughtSignature": "context_engineering_is_the_way_to_go" + } + ] + }, + { + "role": "user", + "parts": [ + { + "functionResponse": { + "name": "get_weather", + "response": { + "content": "15 degrees" + } + } + } + ] + }, + { + "role": "user", + "parts": [ + { + "text": "Summarize." + } + ] + } + ], + "safetySettings": [ + { + "category": "HARM_CATEGORY_HARASSMENT", + "threshold": "OFF" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "threshold": "OFF" + }, + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "threshold": "OFF" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "threshold": "OFF" + } + ], + "generationConfig": { + "maxOutputTokens": 1024 + }, + "tools": [ + { + "functionDeclarations": [ + { + "description": "Get weather by city", + "name": "get_weather", + "parameters": { + "properties": { + "city": { + "type": "STRING" + } + }, + "required": [ + "city" + ], + "type": "OBJECT" + } + } + ] + } + ], + "toolConfig": { + "functionCallingConfig": { + "mode": "AUTO" + } + }, + "systemInstruction": { + "parts": [ + { + "text": "You are a helpful assistant." + } + ] + } +} diff --git a/relaykit/relayconvert/testdata/golden/request/openai_to_openai_responses.golden.json b/relaykit/relayconvert/testdata/golden/request/openai_to_openai_responses.golden.json new file mode 100644 index 00000000..4b661a18 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/request/openai_to_openai_responses.golden.json @@ -0,0 +1,59 @@ +{ + "model": "gpt-test", + "input": [ + { + "content": [ + { + "text": "What is in this image?", + "type": "input_text" + }, + { + "image_url": "https://example.com/cat.png", + "type": "input_image" + } + ], + "role": "user" + }, + { + "content": "", + "role": "assistant" + }, + { + "arguments": "{\"city\":\"Paris\"}", + "call_id": "call_abc", + "name": "get_weather", + "type": "function_call" + }, + { + "call_id": "call_abc", + "output": "15 degrees", + "type": "function_call_output" + }, + { + "content": "Summarize.", + "role": "user" + } + ], + "instructions": "You are a helpful assistant.", + "max_output_tokens": 1024, + "stream": true, + "tool_choice": "auto", + "tools": [ + { + "description": "Get weather by city", + "name": "get_weather", + "parameters": { + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "type": "object" + }, + "type": "function" + } + ] +} diff --git a/relaykit/relayconvert/testdata/golden/response/claude_to_gemini.golden.json b/relaykit/relayconvert/testdata/golden/response/claude_to_gemini.golden.json new file mode 100644 index 00000000..93cd2c91 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/response/claude_to_gemini.golden.json @@ -0,0 +1,66 @@ +{ + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "The answer is 42." + }, + { + "functionCall": { + "name": "get_weather", + "args": { + "city": "Paris" + } + } + } + ] + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [] + } + ], + "usageMetadata": { + "promptTokenCount": 15, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 5, + "totalTokenCount": 20, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": null, + "toolUsePromptTokensDetails": null, + "candidatesTokensDetails": null, + "billing_usage": { + "source": "oai_chat", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 15, + "completion_tokens": 5, + "total_tokens": 20, + "usage_semantic": "openai", + "usage_source": "anthropic", + "prompt_tokens_details": { + "cached_tokens": 3, + "cached_creation_tokens": 2, + "cache_write_tokens": 2, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 15, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 2, + "claude_cache_creation_1_h_tokens": 0 + } + } + } +} diff --git a/relaykit/relayconvert/testdata/golden/response/claude_to_openai.golden.json b/relaykit/relayconvert/testdata/golden/response/claude_to_openai.golden.json new file mode 100644 index 00000000..376e8f7a --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/response/claude_to_openai.golden.json @@ -0,0 +1,64 @@ +{ + "id": "msg_fixed", + "model": "claude-test", + "object": "chat.completion", + "created": 0, + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The answer is 42.", + "tool_calls": [ + { + "id": "toolu_abc", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"city\":\"Paris\"}" + } + } + ] + }, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 15, + "completion_tokens": 5, + "total_tokens": 20, + "usage_semantic": "openai", + "usage_source": "anthropic", + "billing_usage": { + "source": "claude_messages", + "semantic": "anthropic", + "claude_usage": { + "input_tokens": 10, + "cache_creation_input_tokens": 2, + "cache_read_input_tokens": 3, + "output_tokens": 5, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 3, + "cached_creation_tokens": 2, + "cache_write_tokens": 2, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 15, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 2, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/relaykit/relayconvert/testdata/golden/response/claude_to_openai_responses.golden.json b/relaykit/relayconvert/testdata/golden/response/claude_to_openai_responses.golden.json new file mode 100644 index 00000000..e27e09a7 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/response/claude_to_openai_responses.golden.json @@ -0,0 +1,92 @@ +{ + "id": "msg_fixed", + "object": "response", + "created_at": 0, + "status": "completed", + "instructions": null, + "max_output_tokens": 0, + "model": "claude-test", + "output": [ + { + "type": "message", + "id": "msg_fixed_msg_0", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "The answer is 42.", + "annotations": [] + } + ], + "quality": "", + "size": "" + }, + { + "type": "function_call", + "id": "toolu_abc", + "status": "completed", + "role": "", + "content": null, + "quality": "", + "size": "", + "call_id": "toolu_abc", + "name": "get_weather", + "arguments": "{\"city\":\"Paris\"}" + } + ], + "parallel_tool_calls": false, + "previous_response_id": null, + "reasoning": null, + "store": false, + "temperature": 0, + "tool_choice": null, + "tools": null, + "top_p": 0, + "truncation": null, + "usage": { + "prompt_tokens": 15, + "completion_tokens": 5, + "total_tokens": 20, + "usage_semantic": "openai", + "usage_source": "anthropic", + "billing_usage": { + "source": "claude_messages", + "semantic": "anthropic", + "claude_usage": { + "input_tokens": 10, + "cache_creation_input_tokens": 2, + "cache_read_input_tokens": 3, + "output_tokens": 5, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 15, + "output_tokens": 5, + "input_tokens_details": { + "cached_tokens": 3, + "cached_creation_tokens": 2, + "cache_write_tokens": 2, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "claude_cache_creation_5_m_tokens": 2, + "claude_cache_creation_1_h_tokens": 0 + }, + "user": null, + "metadata": null +} diff --git a/relaykit/relayconvert/testdata/golden/response/gemini_to_claude.golden.json b/relaykit/relayconvert/testdata/golden/response/gemini_to_claude.golden.json new file mode 100644 index 00000000..2f3d2492 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/response/gemini_to_claude.golden.json @@ -0,0 +1,55 @@ +{ + "id": "chatcmpl-", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "The answer is 42." + }, + { + "type": "tool_use", + "id": "call_", + "name": "get_weather", + "input": { + "city": "Paris" + } + } + ], + "stop_reason": "tool_use", + "model": "upstream-model", + "usage": { + "input_tokens": 10, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 7, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0, + "billing_usage": { + "source": "oai_chat", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 10, + "completion_tokens": 7, + "total_tokens": 15, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 10, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 2 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + } + } +} diff --git a/relaykit/relayconvert/testdata/golden/response/gemini_to_openai.golden.json b/relaykit/relayconvert/testdata/golden/response/gemini_to_openai.golden.json new file mode 100644 index 00000000..0454e21b --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/response/gemini_to_openai.golden.json @@ -0,0 +1,63 @@ +{ + "id": "chatcmpl-", + "model": "upstream-model", + "object": "chat.completion", + "created": 0, + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The answer is 42.", + "tool_calls": [ + { + "id": "call_", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"city\":\"Paris\"}" + } + } + ] + }, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 7, + "total_tokens": 15, + "billing_usage": { + "source": "gemini_chat", + "semantic": "gemini", + "gemini_usage_metadata": { + "promptTokenCount": 10, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 5, + "totalTokenCount": 15, + "thoughtsTokenCount": 2, + "cachedContentTokenCount": 0, + "promptTokensDetails": [], + "toolUsePromptTokensDetails": [], + "candidatesTokensDetails": [] + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 10, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 2 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/relaykit/relayconvert/testdata/golden/response/gemini_to_openai_responses.golden.json b/relaykit/relayconvert/testdata/golden/response/gemini_to_openai_responses.golden.json new file mode 100644 index 00000000..6992c09e --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/response/gemini_to_openai_responses.golden.json @@ -0,0 +1,91 @@ +{ + "id": "chatcmpl-", + "object": "response", + "created_at": 0, + "status": "completed", + "instructions": null, + "max_output_tokens": 0, + "model": "upstream-model", + "output": [ + { + "type": "message", + "id": "chatcmpl-_msg_0", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "The answer is 42.", + "annotations": [] + } + ], + "quality": "", + "size": "" + }, + { + "type": "function_call", + "id": "call_", + "status": "completed", + "role": "", + "content": null, + "quality": "", + "size": "", + "call_id": "call_", + "name": "get_weather", + "arguments": "{\"city\":\"Paris\"}" + } + ], + "parallel_tool_calls": false, + "previous_response_id": null, + "reasoning": null, + "store": false, + "temperature": 0, + "tool_choice": null, + "tools": null, + "top_p": 0, + "truncation": null, + "usage": { + "prompt_tokens": 10, + "completion_tokens": 7, + "total_tokens": 15, + "billing_usage": { + "source": "gemini_chat", + "semantic": "gemini", + "gemini_usage_metadata": { + "promptTokenCount": 10, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 5, + "totalTokenCount": 15, + "thoughtsTokenCount": 2, + "cachedContentTokenCount": 0, + "promptTokensDetails": [], + "toolUsePromptTokensDetails": [], + "candidatesTokensDetails": [] + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 2 + }, + "input_tokens": 10, + "output_tokens": 7, + "input_tokens_details": { + "cached_tokens": 0, + "text_tokens": 10, + "audio_tokens": 0, + "image_tokens": 0 + }, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + }, + "user": null, + "metadata": null +} diff --git a/relaykit/relayconvert/testdata/golden/response/openai_responses_to_claude.golden.json b/relaykit/relayconvert/testdata/golden/response/openai_responses_to_claude.golden.json new file mode 100644 index 00000000..a5d42e10 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/response/openai_responses_to_claude.golden.json @@ -0,0 +1,55 @@ +{ + "id": "resp_fixed", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "The answer is 42." + }, + { + "type": "tool_use", + "id": "call_abc", + "name": "get_weather", + "input": { + "city": "Paris" + } + } + ], + "stop_reason": "tool_use", + "model": "gpt-test", + "usage": { + "input_tokens": 10, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 5, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0, + "billing_usage": { + "source": "oai_responses", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 15, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 10, + "output_tokens": 5, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + } + } +} diff --git a/relaykit/relayconvert/testdata/golden/response/openai_responses_to_gemini.golden.json b/relaykit/relayconvert/testdata/golden/response/openai_responses_to_gemini.golden.json new file mode 100644 index 00000000..9348f4f7 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/response/openai_responses_to_gemini.golden.json @@ -0,0 +1,62 @@ +{ + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "The answer is 42." + }, + { + "functionCall": { + "name": "get_weather", + "args": { + "city": "Paris" + } + } + } + ] + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [] + } + ], + "usageMetadata": { + "promptTokenCount": 10, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 5, + "totalTokenCount": 15, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": null, + "toolUsePromptTokensDetails": null, + "candidatesTokensDetails": null, + "billing_usage": { + "source": "oai_responses", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 15, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 10, + "output_tokens": 5, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + } + } +} diff --git a/relaykit/relayconvert/testdata/golden/response/openai_responses_to_openai.golden.json b/relaykit/relayconvert/testdata/golden/response/openai_responses_to_openai.golden.json new file mode 100644 index 00000000..17b4b4f4 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/response/openai_responses_to_openai.golden.json @@ -0,0 +1,74 @@ +{ + "id": "resp_fixed", + "model": "gpt-test", + "object": "chat.completion", + "created": 0, + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The answer is 42.", + "tool_calls": [ + { + "id": "call_abc", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"city\":\"Paris\"}" + } + } + ] + }, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "billing_usage": { + "source": "oai_responses", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 15, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 10, + "output_tokens": 5, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 10, + "output_tokens": 5, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/relaykit/relayconvert/testdata/golden/response/openai_to_claude.golden.json b/relaykit/relayconvert/testdata/golden/response/openai_to_claude.golden.json new file mode 100644 index 00000000..b6d3c2fc --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/response/openai_to_claude.golden.json @@ -0,0 +1,55 @@ +{ + "id": "chatcmpl-fixed", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "The answer is 42." + }, + { + "type": "tool_use", + "id": "call_abc", + "name": "get_weather", + "input": { + "city": "Paris" + } + } + ], + "stop_reason": "tool_use", + "model": "gpt-test", + "usage": { + "input_tokens": 10, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 3, + "output_tokens": 5, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0, + "billing_usage": { + "source": "oai_chat", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "prompt_tokens_details": { + "cached_tokens": 3, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 2 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + } + } +} diff --git a/relaykit/relayconvert/testdata/golden/response/openai_to_gemini.golden.json b/relaykit/relayconvert/testdata/golden/response/openai_to_gemini.golden.json new file mode 100644 index 00000000..3eff897a --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/response/openai_to_gemini.golden.json @@ -0,0 +1,62 @@ +{ + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "The answer is 42." + }, + { + "functionCall": { + "name": "get_weather", + "args": { + "city": "Paris" + } + } + } + ] + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [] + } + ], + "usageMetadata": { + "promptTokenCount": 10, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 5, + "totalTokenCount": 15, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": null, + "toolUsePromptTokensDetails": null, + "candidatesTokensDetails": null, + "billing_usage": { + "source": "oai_chat", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "prompt_tokens_details": { + "cached_tokens": 3, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 2 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + } + } +} diff --git a/relaykit/relayconvert/testdata/golden/response/openai_to_openai_responses.golden.json b/relaykit/relayconvert/testdata/golden/response/openai_to_openai_responses.golden.json new file mode 100644 index 00000000..36654404 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/response/openai_to_openai_responses.golden.json @@ -0,0 +1,117 @@ +{ + "id": "chatcmpl-fixed", + "object": "response", + "created_at": 0, + "status": "completed", + "instructions": null, + "max_output_tokens": 0, + "model": "gpt-test", + "output": [ + { + "type": "message", + "id": "chatcmpl-fixed_msg_0", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "The answer is 42.", + "annotations": [] + } + ], + "quality": "", + "size": "" + }, + { + "type": "reasoning", + "id": "chatcmpl-fixed_reasoning_0", + "status": "completed", + "role": "", + "content": [ + { + "type": "summary_text", + "text": "Deep thought.", + "annotations": null + } + ], + "quality": "", + "size": "" + }, + { + "type": "function_call", + "id": "call_abc", + "status": "completed", + "role": "", + "content": null, + "quality": "", + "size": "", + "call_id": "call_abc", + "name": "get_weather", + "arguments": "{\"city\":\"Paris\"}" + } + ], + "parallel_tool_calls": false, + "previous_response_id": null, + "reasoning": null, + "store": false, + "temperature": 0, + "tool_choice": null, + "tools": null, + "top_p": 0, + "truncation": null, + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "billing_usage": { + "source": "oai_chat", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "prompt_tokens_details": { + "cached_tokens": 3, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 2 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 2 + }, + "input_tokens": 10, + "output_tokens": 5, + "input_tokens_details": { + "cached_tokens": 3, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + }, + "user": null, + "metadata": null +} diff --git a/relaykit/relayconvert/testdata/golden/stream/claude_to_gemini.golden.json b/relaykit/relayconvert/testdata/golden/stream/claude_to_gemini.golden.json new file mode 100644 index 00000000..18b47522 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/stream/claude_to_gemini.golden.json @@ -0,0 +1,120 @@ +{ + "events": [ + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "Hello world" + } + ] + }, + "finishReason": null, + "index": 0, + "safetyRatings": [] + } + ], + "usageMetadata": { + "promptTokenCount": 0, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 0, + "totalTokenCount": 0, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": null, + "toolUsePromptTokensDetails": null, + "candidatesTokensDetails": null + } + }, + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [] + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [] + } + ], + "usageMetadata": { + "promptTokenCount": 0, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 2, + "totalTokenCount": 2, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": null, + "toolUsePromptTokensDetails": null, + "candidatesTokensDetails": null, + "billing_usage": { + "source": "oai_chat", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 0, + "completion_tokens": 2, + "total_tokens": 2, + "usage_semantic": "openai", + "usage_source": "anthropic", + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + } + } + } + ], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 2, + "total_tokens": 2, + "usage_semantic": "openai", + "usage_source": "anthropic", + "billing_usage": { + "source": "claude_messages", + "semantic": "anthropic", + "claude_usage": { + "input_tokens": 0, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 2, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 2, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/relaykit/relayconvert/testdata/golden/stream/claude_to_openai.golden.json b/relaykit/relayconvert/testdata/golden/stream/claude_to_openai.golden.json new file mode 100644 index 00000000..6e44485a --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/stream/claude_to_openai.golden.json @@ -0,0 +1,181 @@ +{ + "events": [ + { + "id": "msg_fixed", + "object": "chat.completion.chunk", + "created": 0, + "model": "claude-test", + "system_fingerprint": null, + "choices": [ + { + "delta": { + "content": "", + "role": "assistant" + }, + "logprobs": null, + "finish_reason": null, + "index": 0 + } + ], + "usage": { + "prompt_tokens": 4, + "completion_tokens": 0, + "total_tokens": 4, + "usage_semantic": "openai", + "usage_source": "anthropic", + "billing_usage": { + "source": "claude_messages", + "semantic": "anthropic", + "claude_usage": { + "input_tokens": 4, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 0, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 4, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + { + "id": "", + "object": "chat.completion.chunk", + "created": 0, + "model": "", + "system_fingerprint": null, + "choices": [ + { + "delta": { + "content": "" + }, + "logprobs": null, + "finish_reason": null, + "index": 0 + } + ], + "usage": null + }, + { + "id": "", + "object": "chat.completion.chunk", + "created": 0, + "model": "", + "system_fingerprint": null, + "choices": [ + { + "delta": { + "content": "Hello world" + }, + "logprobs": null, + "finish_reason": null, + "index": 0 + } + ], + "usage": null + }, + { + "id": "", + "object": "chat.completion.chunk", + "created": 0, + "model": "", + "system_fingerprint": null, + "choices": [ + { + "delta": {}, + "logprobs": null, + "finish_reason": "stop", + "index": 0 + } + ], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 2, + "total_tokens": 2, + "usage_semantic": "openai", + "usage_source": "anthropic", + "billing_usage": { + "source": "claude_messages", + "semantic": "anthropic", + "claude_usage": { + "input_tokens": 0, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 2, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + } + ], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 2, + "total_tokens": 2, + "usage_semantic": "openai", + "usage_source": "anthropic", + "billing_usage": { + "source": "claude_messages", + "semantic": "anthropic", + "claude_usage": { + "input_tokens": 0, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 2, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/relaykit/relayconvert/testdata/golden/stream/claude_to_openai_responses.golden.json b/relaykit/relayconvert/testdata/golden/stream/claude_to_openai_responses.golden.json new file mode 100644 index 00000000..2fe0ebbd --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/stream/claude_to_openai_responses.golden.json @@ -0,0 +1,204 @@ +{ + "events": [ + { + "Type": "response.created", + "Payload": { + "type": "response.created", + "response": { + "id": "stream_fixed", + "object": "response", + "created_at": 0, + "status": "in_progress", + "instructions": null, + "max_output_tokens": 0, + "model": "stream-model", + "output": [], + "parallel_tool_calls": false, + "previous_response_id": null, + "reasoning": null, + "store": false, + "temperature": 0, + "tool_choice": null, + "tools": null, + "top_p": 0, + "truncation": null, + "usage": null, + "user": null, + "metadata": null + } + } + }, + { + "Type": "response.output_item.added", + "Payload": { + "type": "response.output_item.added", + "item": { + "type": "message", + "id": "stream_fixed_msg_0", + "status": "in_progress", + "role": "assistant", + "content": [], + "quality": "", + "size": "" + }, + "output_index": 0 + } + }, + { + "Type": "response.output_text.delta", + "Payload": { + "type": "response.output_text.delta", + "delta": "Hello world", + "output_index": 0, + "content_index": 0, + "item_id": "stream_fixed_msg_0" + } + }, + { + "Type": "response.output_text.done", + "Payload": { + "type": "response.output_text.done", + "output_index": 0, + "content_index": 0, + "item_id": "stream_fixed_msg_0" + } + }, + { + "Type": "response.output_item.done", + "Payload": { + "type": "response.output_item.done", + "item": { + "type": "message", + "id": "stream_fixed_msg_0", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Hello world", + "annotations": [] + } + ], + "quality": "", + "size": "" + }, + "output_index": 0 + } + }, + { + "Type": "response.completed", + "Payload": { + "type": "response.completed", + "response": { + "id": "stream_fixed", + "object": "response", + "created_at": 0, + "status": "completed", + "instructions": null, + "max_output_tokens": 0, + "model": "stream-model", + "output": [ + { + "type": "message", + "id": "stream_fixed_msg_0", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Hello world", + "annotations": [] + } + ], + "quality": "", + "size": "" + } + ], + "parallel_tool_calls": false, + "previous_response_id": null, + "reasoning": null, + "store": false, + "temperature": 0, + "tool_choice": null, + "tools": null, + "top_p": 0, + "truncation": null, + "usage": { + "prompt_tokens": 0, + "completion_tokens": 2, + "total_tokens": 2, + "usage_semantic": "openai", + "usage_source": "anthropic", + "billing_usage": { + "source": "claude_messages", + "semantic": "anthropic", + "claude_usage": { + "input_tokens": 0, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 2, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 2, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + }, + "user": null, + "metadata": null + } + } + } + ], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 2, + "total_tokens": 2, + "usage_semantic": "openai", + "usage_source": "anthropic", + "billing_usage": { + "source": "claude_messages", + "semantic": "anthropic", + "claude_usage": { + "input_tokens": 0, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 2, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 2, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/relaykit/relayconvert/testdata/golden/stream/gemini_to_claude.golden.json b/relaykit/relayconvert/testdata/golden/stream/gemini_to_claude.golden.json new file mode 100644 index 00000000..9ea6b5dc --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/stream/gemini_to_claude.golden.json @@ -0,0 +1,88 @@ +{ + "events": [ + { + "type": "message_start", + "message": { + "type": "message", + "model": "upstream-model", + "usage": { + "input_tokens": 0, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 0, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + }, + "role": "assistant", + "id": "chatcmpl-", + "content": [] + } + }, + { + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "text", + "text": "" + } + }, + { + "type": "content_block_delta", + "index": 0, + "delta": { + "type": "text_delta", + "text": "Hello" + } + }, + { + "type": "content_block_delta", + "index": 0, + "delta": { + "type": "text_delta", + "text": " world" + } + } + ], + "usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "billing_usage": { + "source": "gemini_chat", + "semantic": "gemini", + "gemini_usage_metadata": { + "promptTokenCount": 4, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 2, + "totalTokenCount": 6, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": [], + "toolUsePromptTokensDetails": [], + "candidatesTokensDetails": [] + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 4, + "output_tokens": 2, + "input_tokens_details": { + "cached_tokens": 0, + "text_tokens": 4, + "audio_tokens": 0, + "image_tokens": 0 + }, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/relaykit/relayconvert/testdata/golden/stream/gemini_to_openai.golden.json b/relaykit/relayconvert/testdata/golden/stream/gemini_to_openai.golden.json new file mode 100644 index 00000000..7794c776 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/stream/gemini_to_openai.golden.json @@ -0,0 +1,134 @@ +{ + "events": [ + { + "id": "chatcmpl-", + "object": "chat.completion.chunk", + "created": 0, + "model": "upstream-model", + "system_fingerprint": null, + "choices": [ + { + "delta": { + "content": "Hello" + }, + "logprobs": null, + "finish_reason": null, + "index": 0 + } + ], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + { + "id": "chatcmpl-", + "object": "chat.completion.chunk", + "created": 0, + "model": "upstream-model", + "system_fingerprint": null, + "choices": [ + { + "delta": { + "content": " world" + }, + "logprobs": null, + "finish_reason": null, + "index": 0 + } + ], + "usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "billing_usage": { + "source": "gemini_chat", + "semantic": "gemini", + "gemini_usage_metadata": { + "promptTokenCount": 4, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 2, + "totalTokenCount": 6, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": [], + "toolUsePromptTokensDetails": [], + "candidatesTokensDetails": [] + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 4, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + } + ], + "usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "billing_usage": { + "source": "gemini_chat", + "semantic": "gemini", + "gemini_usage_metadata": { + "promptTokenCount": 4, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 2, + "totalTokenCount": 6, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": [], + "toolUsePromptTokensDetails": [], + "candidatesTokensDetails": [] + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 4, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/relaykit/relayconvert/testdata/golden/stream/gemini_to_openai_responses.golden.json b/relaykit/relayconvert/testdata/golden/stream/gemini_to_openai_responses.golden.json new file mode 100644 index 00000000..0a5429c9 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/stream/gemini_to_openai_responses.golden.json @@ -0,0 +1,226 @@ +{ + "events": [ + { + "Type": "response.created", + "Payload": { + "type": "response.created", + "response": { + "id": "stream_fixed", + "object": "response", + "created_at": 0, + "status": "in_progress", + "instructions": null, + "max_output_tokens": 0, + "model": "stream-model", + "output": [], + "parallel_tool_calls": false, + "previous_response_id": null, + "reasoning": null, + "store": false, + "temperature": 0, + "tool_choice": null, + "tools": null, + "top_p": 0, + "truncation": null, + "usage": null, + "user": null, + "metadata": null + } + } + }, + { + "Type": "response.output_item.added", + "Payload": { + "type": "response.output_item.added", + "item": { + "type": "message", + "id": "stream_fixed_msg_0", + "status": "in_progress", + "role": "assistant", + "content": [], + "quality": "", + "size": "" + }, + "output_index": 0 + } + }, + { + "Type": "response.output_text.delta", + "Payload": { + "type": "response.output_text.delta", + "delta": "Hello", + "output_index": 0, + "content_index": 0, + "item_id": "stream_fixed_msg_0" + } + }, + { + "Type": "response.output_text.delta", + "Payload": { + "type": "response.output_text.delta", + "delta": " world", + "output_index": 0, + "content_index": 0, + "item_id": "stream_fixed_msg_0" + } + }, + { + "Type": "response.output_text.done", + "Payload": { + "type": "response.output_text.done", + "output_index": 0, + "content_index": 0, + "item_id": "stream_fixed_msg_0" + } + }, + { + "Type": "response.output_item.done", + "Payload": { + "type": "response.output_item.done", + "item": { + "type": "message", + "id": "stream_fixed_msg_0", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Hello world", + "annotations": [] + } + ], + "quality": "", + "size": "" + }, + "output_index": 0 + } + }, + { + "Type": "response.completed", + "Payload": { + "type": "response.completed", + "response": { + "id": "stream_fixed", + "object": "response", + "created_at": 0, + "status": "completed", + "instructions": null, + "max_output_tokens": 0, + "model": "stream-model", + "output": [ + { + "type": "message", + "id": "stream_fixed_msg_0", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Hello world", + "annotations": [] + } + ], + "quality": "", + "size": "" + } + ], + "parallel_tool_calls": false, + "previous_response_id": null, + "reasoning": null, + "store": false, + "temperature": 0, + "tool_choice": null, + "tools": null, + "top_p": 0, + "truncation": null, + "usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "billing_usage": { + "source": "gemini_chat", + "semantic": "gemini", + "gemini_usage_metadata": { + "promptTokenCount": 4, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 2, + "totalTokenCount": 6, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": [], + "toolUsePromptTokensDetails": [], + "candidatesTokensDetails": [] + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 4, + "output_tokens": 2, + "input_tokens_details": { + "cached_tokens": 0, + "text_tokens": 4, + "audio_tokens": 0, + "image_tokens": 0 + }, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + }, + "user": null, + "metadata": null + } + } + } + ], + "usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "billing_usage": { + "source": "gemini_chat", + "semantic": "gemini", + "gemini_usage_metadata": { + "promptTokenCount": 4, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 2, + "totalTokenCount": 6, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": [], + "toolUsePromptTokensDetails": [], + "candidatesTokensDetails": [] + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 4, + "output_tokens": 2, + "input_tokens_details": { + "cached_tokens": 0, + "text_tokens": 4, + "audio_tokens": 0, + "image_tokens": 0 + }, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/relaykit/relayconvert/testdata/golden/stream/openai_responses_to_claude.golden.json b/relaykit/relayconvert/testdata/golden/stream/openai_responses_to_claude.golden.json new file mode 100644 index 00000000..49cd6683 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/stream/openai_responses_to_claude.golden.json @@ -0,0 +1,94 @@ +{ + "events": [ + { + "type": "message_start", + "message": { + "type": "message", + "model": "stream-model", + "usage": { + "input_tokens": 0, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 0, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + }, + "role": "assistant", + "id": "stream_fixed", + "content": [] + } + }, + { + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "text", + "text": "" + } + }, + { + "type": "content_block_delta", + "index": 0, + "delta": { + "type": "text_delta", + "text": "Hello" + } + }, + { + "type": "content_block_delta", + "index": 0, + "delta": { + "type": "text_delta", + "text": " world" + } + } + ], + "usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "billing_usage": { + "source": "oai_responses", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 6, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 4, + "output_tokens": 2, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 4, + "output_tokens": 2, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/relaykit/relayconvert/testdata/golden/stream/openai_responses_to_gemini.golden.json b/relaykit/relayconvert/testdata/golden/stream/openai_responses_to_gemini.golden.json new file mode 100644 index 00000000..cbeb4d2d --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/stream/openai_responses_to_gemini.golden.json @@ -0,0 +1,132 @@ +{ + "events": [ + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "Hello" + } + ] + }, + "finishReason": null, + "index": 0, + "safetyRatings": [] + } + ], + "usageMetadata": { + "promptTokenCount": 0, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 0, + "totalTokenCount": 0, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": null, + "toolUsePromptTokensDetails": null, + "candidatesTokensDetails": null + } + }, + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": " world" + } + ] + }, + "finishReason": null, + "index": 0, + "safetyRatings": [] + } + ], + "usageMetadata": { + "promptTokenCount": 0, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 0, + "totalTokenCount": 0, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": null, + "toolUsePromptTokensDetails": null, + "candidatesTokensDetails": null + } + }, + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [] + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [] + } + ], + "usageMetadata": { + "promptTokenCount": 0, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 0, + "totalTokenCount": 0, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": null, + "toolUsePromptTokensDetails": null, + "candidatesTokensDetails": null + } + } + ], + "usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "billing_usage": { + "source": "oai_responses", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 6, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 4, + "output_tokens": 2, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 4, + "output_tokens": 2, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/relaykit/relayconvert/testdata/golden/stream/openai_responses_to_openai.golden.json b/relaykit/relayconvert/testdata/golden/stream/openai_responses_to_openai.golden.json new file mode 100644 index 00000000..246b3ffa --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/stream/openai_responses_to_openai.golden.json @@ -0,0 +1,123 @@ +{ + "events": [ + { + "id": "stream_fixed", + "object": "chat.completion.chunk", + "created": 0, + "model": "stream-model", + "system_fingerprint": null, + "choices": [ + { + "delta": { + "content": "", + "role": "assistant" + }, + "logprobs": null, + "finish_reason": null, + "index": 0 + } + ], + "usage": null + }, + { + "id": "stream_fixed", + "object": "chat.completion.chunk", + "created": 0, + "model": "stream-model", + "system_fingerprint": null, + "choices": [ + { + "delta": { + "content": "Hello" + }, + "logprobs": null, + "finish_reason": null, + "index": 0 + } + ], + "usage": null + }, + { + "id": "stream_fixed", + "object": "chat.completion.chunk", + "created": 0, + "model": "stream-model", + "system_fingerprint": null, + "choices": [ + { + "delta": { + "content": " world" + }, + "logprobs": null, + "finish_reason": null, + "index": 0 + } + ], + "usage": null + }, + { + "id": "stream_fixed", + "object": "chat.completion.chunk", + "created": 0, + "model": "gpt-test", + "system_fingerprint": null, + "choices": [ + { + "delta": {}, + "logprobs": null, + "finish_reason": "stop", + "index": 0 + } + ], + "usage": null + } + ], + "usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "billing_usage": { + "source": "oai_responses", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 6, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 4, + "output_tokens": 2, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 4, + "output_tokens": 2, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/relaykit/relayconvert/testdata/golden/stream/openai_to_claude.golden.json b/relaykit/relayconvert/testdata/golden/stream/openai_to_claude.golden.json new file mode 100644 index 00000000..a1b058f7 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/stream/openai_to_claude.golden.json @@ -0,0 +1,141 @@ +{ + "events": [ + { + "type": "message_start", + "message": { + "type": "message", + "model": "gpt-test", + "usage": { + "input_tokens": 0, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 0, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + }, + "role": "assistant", + "id": "chatcmpl-fixed", + "content": [] + } + }, + { + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "text", + "text": "" + } + }, + { + "type": "content_block_delta", + "index": 0, + "delta": { + "type": "text_delta", + "text": "Hello" + } + }, + { + "type": "content_block_delta", + "index": 0, + "delta": { + "type": "text_delta", + "text": " world" + } + }, + { + "type": "content_block_stop", + "index": 0 + }, + { + "type": "message_delta", + "usage": { + "input_tokens": 4, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 2, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0, + "billing_usage": { + "source": "oai_chat", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + } + }, + "delta": { + "stop_reason": "end_turn" + } + }, + { + "type": "message_stop" + } + ], + "usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "billing_usage": { + "source": "oai_chat", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 4, + "output_tokens": 2, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/relaykit/relayconvert/testdata/golden/stream/openai_to_gemini.golden.json b/relaykit/relayconvert/testdata/golden/stream/openai_to_gemini.golden.json new file mode 100644 index 00000000..3c9aee69 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/stream/openai_to_gemini.golden.json @@ -0,0 +1,158 @@ +{ + "events": [ + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "Hello" + } + ] + }, + "finishReason": null, + "index": 0, + "safetyRatings": [] + } + ], + "usageMetadata": { + "promptTokenCount": 0, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 0, + "totalTokenCount": 0, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": null, + "toolUsePromptTokensDetails": null, + "candidatesTokensDetails": null + } + }, + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": " world" + } + ] + }, + "finishReason": null, + "index": 0, + "safetyRatings": [] + } + ], + "usageMetadata": { + "promptTokenCount": 0, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 0, + "totalTokenCount": 0, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": null, + "toolUsePromptTokensDetails": null, + "candidatesTokensDetails": null + } + }, + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [] + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [] + } + ], + "usageMetadata": { + "promptTokenCount": 4, + "toolUsePromptTokenCount": 0, + "candidatesTokenCount": 2, + "totalTokenCount": 6, + "thoughtsTokenCount": 0, + "cachedContentTokenCount": 0, + "promptTokensDetails": null, + "toolUsePromptTokensDetails": null, + "candidatesTokensDetails": null, + "billing_usage": { + "source": "oai_chat", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + } + } + } + ], + "usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "billing_usage": { + "source": "oai_chat", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 4, + "output_tokens": 2, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/relaykit/relayconvert/testdata/golden/stream/openai_to_openai_responses.golden.json b/relaykit/relayconvert/testdata/golden/stream/openai_to_openai_responses.golden.json new file mode 100644 index 00000000..f0a41ed6 --- /dev/null +++ b/relaykit/relayconvert/testdata/golden/stream/openai_to_openai_responses.golden.json @@ -0,0 +1,238 @@ +{ + "events": [ + { + "Type": "response.created", + "Payload": { + "type": "response.created", + "response": { + "id": "stream_fixed", + "object": "response", + "created_at": 0, + "status": "in_progress", + "instructions": null, + "max_output_tokens": 0, + "model": "stream-model", + "output": [], + "parallel_tool_calls": false, + "previous_response_id": null, + "reasoning": null, + "store": false, + "temperature": 0, + "tool_choice": null, + "tools": null, + "top_p": 0, + "truncation": null, + "usage": null, + "user": null, + "metadata": null + } + } + }, + { + "Type": "response.output_item.added", + "Payload": { + "type": "response.output_item.added", + "item": { + "type": "message", + "id": "stream_fixed_msg_0", + "status": "in_progress", + "role": "assistant", + "content": [], + "quality": "", + "size": "" + }, + "output_index": 0 + } + }, + { + "Type": "response.output_text.delta", + "Payload": { + "type": "response.output_text.delta", + "delta": "Hello", + "output_index": 0, + "content_index": 0, + "item_id": "stream_fixed_msg_0" + } + }, + { + "Type": "response.output_text.delta", + "Payload": { + "type": "response.output_text.delta", + "delta": " world", + "output_index": 0, + "content_index": 0, + "item_id": "stream_fixed_msg_0" + } + }, + { + "Type": "response.output_text.done", + "Payload": { + "type": "response.output_text.done", + "output_index": 0, + "content_index": 0, + "item_id": "stream_fixed_msg_0" + } + }, + { + "Type": "response.output_item.done", + "Payload": { + "type": "response.output_item.done", + "item": { + "type": "message", + "id": "stream_fixed_msg_0", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Hello world", + "annotations": [] + } + ], + "quality": "", + "size": "" + }, + "output_index": 0 + } + }, + { + "Type": "response.completed", + "Payload": { + "type": "response.completed", + "response": { + "id": "stream_fixed", + "object": "response", + "created_at": 0, + "status": "completed", + "instructions": null, + "max_output_tokens": 0, + "model": "stream-model", + "output": [ + { + "type": "message", + "id": "stream_fixed_msg_0", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Hello world", + "annotations": [] + } + ], + "quality": "", + "size": "" + } + ], + "parallel_tool_calls": false, + "previous_response_id": null, + "reasoning": null, + "store": false, + "temperature": 0, + "tool_choice": null, + "tools": null, + "top_p": 0, + "truncation": null, + "usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "billing_usage": { + "source": "oai_chat", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 4, + "output_tokens": 2, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + }, + "user": null, + "metadata": null + } + } + } + ], + "usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "billing_usage": { + "source": "oai_chat", + "semantic": "openai", + "openai_usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 0, + "output_tokens": 0, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } + }, + "prompt_tokens_details": { + "cached_tokens": 0, + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0 + }, + "completion_tokens_details": { + "text_tokens": 0, + "audio_tokens": 0, + "image_tokens": 0, + "reasoning_tokens": 0 + }, + "input_tokens": 4, + "output_tokens": 2, + "input_tokens_details": null, + "claude_cache_creation_5_m_tokens": 0, + "claude_cache_creation_1_h_tokens": 0 + } +} diff --git a/service/relayconvert/text_converter_registry.go b/relaykit/relayconvert/text_converter_registry.go similarity index 99% rename from service/relayconvert/text_converter_registry.go rename to relaykit/relayconvert/text_converter_registry.go index d8cf8362..49e19379 100644 --- a/service/relayconvert/text_converter_registry.go +++ b/relaykit/relayconvert/text_converter_registry.go @@ -5,7 +5,7 @@ import ( "strings" "sync" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" ) type TextConverterQuality string diff --git a/service/relayconvert/text_converter_registry_test.go b/relaykit/relayconvert/text_converter_registry_test.go similarity index 99% rename from service/relayconvert/text_converter_registry_test.go rename to relaykit/relayconvert/text_converter_registry_test.go index c17f6663..53979572 100644 --- a/service/relayconvert/text_converter_registry_test.go +++ b/relaykit/relayconvert/text_converter_registry_test.go @@ -3,7 +3,7 @@ package relayconvert import ( "testing" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/types/channel_error.go b/relaykit/types/channel_error.go similarity index 100% rename from types/channel_error.go rename to relaykit/types/channel_error.go diff --git a/relaykit/types/endpoint_type.go b/relaykit/types/endpoint_type.go new file mode 100644 index 00000000..f2f96fa9 --- /dev/null +++ b/relaykit/types/endpoint_type.go @@ -0,0 +1,30 @@ +package types + +// EndpointType identifies a downstream API surface. Moved from constant so +// the conversion kit (dto/relayconvert) has no host imports; constant keeps +// aliases for host code. +type EndpointType string + +const ( + EndpointTypeOpenAI EndpointType = "openai" + EndpointTypeOpenAIResponse EndpointType = "openai-response" + EndpointTypeOpenAIResponseCompact EndpointType = "openai-response-compact" + EndpointTypeOpenAIAlphaSearch EndpointType = "openai-alpha-search" + EndpointTypeAnthropic EndpointType = "anthropic" + EndpointTypeGemini EndpointType = "gemini" + EndpointTypeJinaRerank EndpointType = "jina-rerank" + EndpointTypeImageGeneration EndpointType = "image-generation" + EndpointTypeEmbeddings EndpointType = "embeddings" + EndpointTypeOpenAIVideo EndpointType = "openai-video" +) + +// Finish reasons shared by the OpenAI-compatible response formats. +// Declared as vars (not consts) because converter code takes their address +// for *string finish-reason fields. +var ( + FinishReasonStop = "stop" + FinishReasonToolCalls = "tool_calls" + FinishReasonLength = "length" + FinishReasonFunctionCall = "function_call" + FinishReasonContentFilter = "content_filter" +) diff --git a/types/error.go b/relaykit/types/error.go similarity index 97% rename from types/error.go rename to relaykit/types/error.go index 9717401a..387fdad7 100644 --- a/types/error.go +++ b/relaykit/types/error.go @@ -7,7 +7,7 @@ import ( "net/http" "strings" - "github.com/QuantumNous/new-api/common" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" ) type OpenAIError struct { @@ -156,7 +156,7 @@ func (e *NewAPIError) MaskSensitiveError() string { if e.errorCode == ErrorCodeCountTokenFailed { return errStr } - return common.MaskSensitiveInfo(errStr) + return kitutil.MaskSensitiveInfo(errStr) } func (e *NewAPIError) MaskSensitiveErrorWithStatusCode() string { @@ -202,7 +202,7 @@ func (e *NewAPIError) ToOpenAIError() OpenAIError { } } if e.errorCode != ErrorCodeCountTokenFailed { - result.Message = common.MaskSensitiveInfo(result.Message) + result.Message = kitutil.MaskSensitiveInfo(result.Message) } if result.Message == "" { result.Message = string(e.errorType) @@ -231,7 +231,7 @@ func (e *NewAPIError) ToClaudeError() ClaudeError { } } if e.errorCode != ErrorCodeCountTokenFailed { - result.Message = common.MaskSensitiveInfo(result.Message) + result.Message = kitutil.MaskSensitiveInfo(result.Message) } if result.Message == "" { result.Message = string(e.errorType) @@ -386,7 +386,7 @@ func ErrOptionWithSkipRetry() NewAPIErrorOptions { func ErrOptionWithNoRecordErrorLog() NewAPIErrorOptions { return func(e *NewAPIError) { - e.recordErrorLog = common.GetPointer(false) + e.recordErrorLog = kitutil.GetPointer(false) } } @@ -398,7 +398,7 @@ func ErrOptionWithStatusCode(statusCode int) NewAPIErrorOptions { func ErrOptionWithHideErrMsg(replaceStr string) NewAPIErrorOptions { return func(e *NewAPIError) { - if common.DebugEnabled { + if kitutil.Debug.Load() { fmt.Printf("ErrOptionWithHideErrMsg: %s, origin error: %s", replaceStr, e.Err) } e.Err = errors.New(replaceStr) diff --git a/types/file_data.go b/relaykit/types/file_data.go similarity index 100% rename from types/file_data.go rename to relaykit/types/file_data.go diff --git a/types/file_source.go b/relaykit/types/file_source.go similarity index 100% rename from types/file_source.go rename to relaykit/types/file_source.go diff --git a/types/price_data.go b/relaykit/types/price_data.go similarity index 100% rename from types/price_data.go rename to relaykit/types/price_data.go diff --git a/types/relay_format.go b/relaykit/types/relay_format.go similarity index 100% rename from types/relay_format.go rename to relaykit/types/relay_format.go diff --git a/types/request_meta.go b/relaykit/types/request_meta.go similarity index 100% rename from types/request_meta.go rename to relaykit/types/request_meta.go diff --git a/types/rw_map.go b/relaykit/types/rw_map.go similarity index 88% rename from types/rw_map.go rename to relaykit/types/rw_map.go index 3d296816..46dd5d8e 100644 --- a/types/rw_map.go +++ b/relaykit/types/rw_map.go @@ -3,7 +3,7 @@ package types import ( "sync" - "github.com/QuantumNous/new-api/common" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" ) type RWMap[K comparable, V any] struct { @@ -15,13 +15,13 @@ func (m *RWMap[K, V]) UnmarshalJSON(b []byte) error { m.mutex.Lock() defer m.mutex.Unlock() m.data = make(map[K]V) - return common.Unmarshal(b, &m.data) + return kitutil.Unmarshal(b, &m.data) } func (m *RWMap[K, V]) MarshalJSON() ([]byte, error) { m.mutex.RLock() defer m.mutex.RUnlock() - return common.Marshal(m.data) + return kitutil.Marshal(m.data) } func NewRWMap[K comparable, V any]() *RWMap[K, V] { @@ -78,7 +78,7 @@ func LoadFromJsonString[K comparable, V any](m *RWMap[K, V], jsonStr string) err m.mutex.Lock() defer m.mutex.Unlock() m.data = make(map[K]V) - return common.Unmarshal([]byte(jsonStr), &m.data) + return kitutil.Unmarshal([]byte(jsonStr), &m.data) } // LoadFromJsonStringWithCallback loads a JSON string into the RWMap and calls the callback on success. @@ -86,7 +86,7 @@ func LoadFromJsonStringWithCallback[K comparable, V any](m *RWMap[K, V], jsonStr m.mutex.Lock() defer m.mutex.Unlock() m.data = make(map[K]V) - err := common.Unmarshal([]byte(jsonStr), &m.data) + err := kitutil.Unmarshal([]byte(jsonStr), &m.data) if err == nil && onSuccess != nil { onSuccess() } diff --git a/types/set.go b/relaykit/types/set.go similarity index 100% rename from types/set.go rename to relaykit/types/set.go diff --git a/router/relay-router.go b/router/relay-router.go index 5fa64ef5..e08ecb14 100644 --- a/router/relay-router.go +++ b/router/relay-router.go @@ -5,7 +5,7 @@ import ( "github.com/QuantumNous/new-api/controller" "github.com/QuantumNous/new-api/middleware" "github.com/QuantumNous/new-api/relay" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/service/billing.go b/service/billing.go index 02a093b6..7ffa5371 100644 --- a/service/billing.go +++ b/service/billing.go @@ -6,7 +6,7 @@ import ( "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/service/billing_session.go b/service/billing_session.go index 32344eaf..96afcf03 100644 --- a/service/billing_session.go +++ b/service/billing_session.go @@ -10,7 +10,7 @@ import ( "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/bytedance/gopkg/util/gopool" "github.com/gin-gonic/gin" diff --git a/service/billing_usage.go b/service/billing_usage.go index a8d47917..2766178d 100644 --- a/service/billing_usage.go +++ b/service/billing_usage.go @@ -3,7 +3,7 @@ package service import ( "strings" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" ) const ( diff --git a/service/channel.go b/service/channel.go index 856e2cde..f348e081 100644 --- a/service/channel.go +++ b/service/channel.go @@ -5,10 +5,10 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/types" ) func formatNotifyType(channelId int, status int) string { diff --git a/service/channel_affinity.go b/service/channel_affinity.go index 96ec13e2..a112fb84 100644 --- a/service/channel_affinity.go +++ b/service/channel_affinity.go @@ -10,10 +10,10 @@ import ( "time" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/pkg/cachex" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/samber/hot" "github.com/tidwall/gjson" diff --git a/service/channel_affinity_usage_cache_test.go b/service/channel_affinity_usage_cache_test.go index 64d3d715..876297b2 100644 --- a/service/channel_affinity_usage_cache_test.go +++ b/service/channel_affinity_usage_cache_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - "github.com/QuantumNous/new-api/dto" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) diff --git a/service/convert.go b/service/convert.go index c62b7d7a..33c5296b 100644 --- a/service/convert.go +++ b/service/convert.go @@ -1,9 +1,9 @@ package service import ( - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/service/relayconvert" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" ) func NormalizeCacheCreationSplit(totalTokens int, tokens5m int, tokens1h int) (int, int) { diff --git a/service/convert_test.go b/service/convert_test.go index 1d07ee07..b6627c12 100644 --- a/service/convert_test.go +++ b/service/convert_test.go @@ -3,8 +3,9 @@ package service import ( "testing" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -64,6 +65,50 @@ func TestStreamResponseConverterFacades(t *testing.T) { require.Len(t, geminiResp.Candidates, 1) } +func TestRequestConverterFacadeAcceptsTypedNilRelayInfo(t *testing.T) { + for _, target := range []types.RelayFormat{types.RelayFormatClaude, types.RelayFormatGemini} { + t.Run(string(target), func(t *testing.T) { + var info *relaycommon.RelayInfo + request := &dto.GeneralOpenAIRequest{ + Model: "test-model", + Messages: []dto.Message{ + {Role: "user", Content: "hello"}, + }, + } + + result, err := ConvertRequest(nil, info, target, request) + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, target, result.To) + }) + } +} + +func TestStreamResponseConverterFacadesAcceptTypedNilRelayInfo(t *testing.T) { + var info *relaycommon.RelayInfo + streamResp := &dto.ChatCompletionsStreamResponse{ + Id: "chatcmpl_typed_nil", + Model: "gpt-test", + Choices: []dto.ChatCompletionsStreamResponseChoice{ + { + Delta: dto.ChatCompletionsStreamResponseChoiceDelta{ + Content: ptrValue("hello"), + }, + }, + }, + } + + claudeResponses := StreamResponseOpenAI2Claude(streamResp, info) + require.NotEmpty(t, claudeResponses) + assert.Equal(t, "content_block_start", claudeResponses[0].Type) + + geminiResp := StreamResponseOpenAI2Gemini(streamResp, info) + require.NotNil(t, geminiResp) + require.Len(t, geminiResp.Candidates, 1) + assert.Zero(t, geminiResp.UsageMetadata.PromptTokenCount) +} + func ptrValue[T any](value T) *T { return &value } diff --git a/service/error.go b/service/error.go index 9d035858..f14f1bba 100644 --- a/service/error.go +++ b/service/error.go @@ -12,20 +12,21 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" + taskdto "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" ) -func MidjourneyErrorWrapper(code int, desc string) *dto.MidjourneyResponse { - return &dto.MidjourneyResponse{ +func MidjourneyErrorWrapper(code int, desc string) *taskdto.MidjourneyResponse { + return &taskdto.MidjourneyResponse{ Code: code, Description: desc, } } -func MidjourneyErrorWithStatusCodeWrapper(code int, desc string, statusCode int) *dto.MidjourneyResponseWithStatusCode { - return &dto.MidjourneyResponseWithStatusCode{ +func MidjourneyErrorWithStatusCodeWrapper(code int, desc string, statusCode int) *taskdto.MidjourneyResponseWithStatusCode { + return &taskdto.MidjourneyResponseWithStatusCode{ StatusCode: statusCode, Response: *MidjourneyErrorWrapper(code, desc), } @@ -190,13 +191,13 @@ func parseStatusCodeMappingValue(value any) (int, bool) { } } -func TaskErrorWrapperLocal(err error, code string, statusCode int) *dto.TaskError { +func TaskErrorWrapperLocal(err error, code string, statusCode int) *taskdto.TaskError { openaiErr := TaskErrorWrapper(err, code, statusCode) openaiErr.LocalError = true return openaiErr } -func TaskErrorWrapper(err error, code string, statusCode int) *dto.TaskError { +func TaskErrorWrapper(err error, code string, statusCode int) *taskdto.TaskError { text := err.Error() lowerText := strings.ToLower(text) if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") { @@ -205,7 +206,7 @@ func TaskErrorWrapper(err error, code string, statusCode int) *dto.TaskError { text = common.MaskSensitiveInfo(text) } //避免暴露内部错误 - taskError := &dto.TaskError{ + taskError := &taskdto.TaskError{ Code: code, Message: text, StatusCode: statusCode, @@ -216,11 +217,11 @@ func TaskErrorWrapper(err error, code string, statusCode int) *dto.TaskError { } // TaskErrorFromAPIError 将 PreConsumeBilling 返回的 NewAPIError 转换为 TaskError。 -func TaskErrorFromAPIError(apiErr *types.NewAPIError) *dto.TaskError { +func TaskErrorFromAPIError(apiErr *types.NewAPIError) *taskdto.TaskError { if apiErr == nil { return nil } - return &dto.TaskError{ + return &taskdto.TaskError{ Code: string(apiErr.GetErrorCode()), Message: apiErr.Err.Error(), StatusCode: apiErr.StatusCode, diff --git a/service/error_test.go b/service/error_test.go index 9f19bfbb..266d2a87 100644 --- a/service/error_test.go +++ b/service/error_test.go @@ -10,7 +10,7 @@ import ( "testing" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) diff --git a/service/file_decoder.go b/service/file_decoder.go index 27605a0f..18376ccb 100644 --- a/service/file_decoder.go +++ b/service/file_decoder.go @@ -13,7 +13,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/logger" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/service/file_service.go b/service/file_service.go index 03baf2de..52652a34 100644 --- a/service/file_service.go +++ b/service/file_service.go @@ -16,7 +16,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/logger" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" "golang.org/x/image/webp" diff --git a/service/log_info_generate.go b/service/log_info_generate.go index 207b0af5..220c511c 100644 --- a/service/log_info_generate.go +++ b/service/log_info_generate.go @@ -7,11 +7,11 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/pkg/billingexpr" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/service/openai_chat_responses_compat.go b/service/openai_chat_responses_compat.go index 806b7662..61d1d6e2 100644 --- a/service/openai_chat_responses_compat.go +++ b/service/openai_chat_responses_compat.go @@ -1,8 +1,8 @@ package service import ( - "github.com/QuantumNous/new-api/dto" - "github.com/QuantumNous/new-api/service/relayconvert" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" ) func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*dto.OpenAIResponsesRequest, error) { diff --git a/service/openai_chat_responses_mode.go b/service/openai_chat_responses_mode.go index 4db37279..1095dc10 100644 --- a/service/openai_chat_responses_mode.go +++ b/service/openai_chat_responses_mode.go @@ -1,14 +1,54 @@ package service import ( - "github.com/QuantumNous/new-api/service/relayconvert" + "regexp" + "sync" + "github.com/QuantumNous/new-api/setting/model_setting" ) +// Chat→Responses upgrade policy is host routing logic (it decides *whether* +// to convert, reading host settings), so it lives here, not in relayconvert. + +var chatResponsesRegexCache sync.Map // map[string]*regexp.Regexp + +func matchAnyModelPattern(patterns []string, model string) bool { + if len(patterns) == 0 || model == "" { + return false + } + for _, pattern := range patterns { + if pattern == "" { + continue + } + re, ok := chatResponsesRegexCache.Load(pattern) + if !ok { + compiled, err := regexp.Compile(pattern) + if err != nil { + // Treat invalid patterns as non-matching to avoid breaking runtime traffic. + continue + } + re = compiled + chatResponsesRegexCache.Store(pattern, re) + } + if re.(*regexp.Regexp).MatchString(model) { + return true + } + } + return false +} + func ShouldChatCompletionsUseResponsesPolicy(policy model_setting.ChatCompletionsToResponsesPolicy, channelID int, channelType int, model string) bool { - return relayconvert.ShouldChatCompletionsUseResponsesPolicy(policy, channelID, channelType, model) + if !policy.IsChannelEnabled(channelID, channelType) { + return false + } + return matchAnyModelPattern(policy.ModelPatterns, model) } func ShouldChatCompletionsUseResponsesGlobal(channelID int, channelType int, model string) bool { - return relayconvert.ShouldChatCompletionsUseResponsesGlobal(channelID, channelType, model) + return ShouldChatCompletionsUseResponsesPolicy( + model_setting.GetGlobalSettings().ChatCompletionsToResponsesPolicy, + channelID, + channelType, + model, + ) } diff --git a/service/quota.go b/service/quota.go index 84ef22b9..92841667 100644 --- a/service/quota.go +++ b/service/quota.go @@ -9,14 +9,14 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/pkg/billingexpr" perfmetrics "github.com/QuantumNous/new-api/pkg/perf_metrics" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/ratio_setting" - "github.com/QuantumNous/new-api/types" "github.com/bytedance/gopkg/util/gopool" diff --git a/service/quota_saturation_test.go b/service/quota_saturation_test.go index 518bdde5..e8cd55c2 100644 --- a/service/quota_saturation_test.go +++ b/service/quota_saturation_test.go @@ -6,7 +6,7 @@ import ( "github.com/QuantumNous/new-api/common" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" diff --git a/service/relayconvert/internal/matcher/regex.go b/service/relayconvert/internal/matcher/regex.go deleted file mode 100644 index 51f0dd9d..00000000 --- a/service/relayconvert/internal/matcher/regex.go +++ /dev/null @@ -1,33 +0,0 @@ -package matcher - -import ( - "regexp" - "sync" -) - -var compiledRegexCache sync.Map // map[string]*regexp.Regexp - -func MatchAnyRegex(patterns []string, s string) bool { - if len(patterns) == 0 || s == "" { - return false - } - for _, pattern := range patterns { - if pattern == "" { - continue - } - re, ok := compiledRegexCache.Load(pattern) - if !ok { - compiled, err := regexp.Compile(pattern) - if err != nil { - // Treat invalid patterns as non-matching to avoid breaking runtime traffic. - continue - } - re = compiled - compiledRegexCache.Store(pattern, re) - } - if re.(*regexp.Regexp).MatchString(s) { - return true - } - } - return false -} diff --git a/service/relayconvert/internal/meta/relay_info.go b/service/relayconvert/internal/meta/relay_info.go deleted file mode 100644 index 926f3c33..00000000 --- a/service/relayconvert/internal/meta/relay_info.go +++ /dev/null @@ -1,17 +0,0 @@ -package meta - -import relaycommon "github.com/QuantumNous/new-api/relay/common" - -func RelayInfoChannelType(info *relaycommon.RelayInfo) int { - if info == nil || info.ChannelMeta == nil { - return 0 - } - return info.ChannelType -} - -func RelayInfoUpstreamModelName(info *relaycommon.RelayInfo) string { - if info == nil || info.ChannelMeta == nil { - return "" - } - return info.UpstreamModelName -} diff --git a/service/relayconvert/internal/oai_chat/to_oai_responses_policy.go b/service/relayconvert/internal/oai_chat/to_oai_responses_policy.go deleted file mode 100644 index 350d847c..00000000 --- a/service/relayconvert/internal/oai_chat/to_oai_responses_policy.go +++ /dev/null @@ -1,22 +0,0 @@ -package oaichat - -import ( - "github.com/QuantumNous/new-api/service/relayconvert/internal/matcher" - "github.com/QuantumNous/new-api/setting/model_setting" -) - -func ShouldChatCompletionsUseResponsesPolicy(policy model_setting.ChatCompletionsToResponsesPolicy, channelID int, channelType int, model string) bool { - if !policy.IsChannelEnabled(channelID, channelType) { - return false - } - return matcher.MatchAnyRegex(policy.ModelPatterns, model) -} - -func ShouldChatCompletionsUseResponsesGlobal(channelID int, channelType int, model string) bool { - return ShouldChatCompletionsUseResponsesPolicy( - model_setting.GetGlobalSettings().ChatCompletionsToResponsesPolicy, - channelID, - channelType, - model, - ) -} diff --git a/service/relayconvert/request_compat.go b/service/relayconvert/request_compat.go deleted file mode 100644 index f2f77ae9..00000000 --- a/service/relayconvert/request_compat.go +++ /dev/null @@ -1,57 +0,0 @@ -package relayconvert - -import ( - "github.com/QuantumNous/new-api/dto" - relaycommon "github.com/QuantumNous/new-api/relay/common" - claudemessages "github.com/QuantumNous/new-api/service/relayconvert/internal/claude_messages" - geminichat "github.com/QuantumNous/new-api/service/relayconvert/internal/gemini_chat" - oaichat "github.com/QuantumNous/new-api/service/relayconvert/internal/oai_chat" - oairesponses "github.com/QuantumNous/new-api/service/relayconvert/internal/oai_responses" - sharedgemini "github.com/QuantumNous/new-api/service/relayconvert/internal/shared/gemini" - "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/gin-gonic/gin" -) - -func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info *relaycommon.RelayInfo) (*dto.GeneralOpenAIRequest, error) { - return claudemessages.ClaudeMessagesRequestToOpenAIChat(claudeRequest, info) -} - -func OpenAIChatRequestToClaudeMessages(c *gin.Context, textRequest dto.GeneralOpenAIRequest) (*dto.ClaudeRequest, error) { - return oaichat.OpenAIChatRequestToClaudeMessages(c, textRequest) -} - -func GeminiGenerateContentRequestToOpenAIChat(geminiRequest *dto.GeminiChatRequest, info *relaycommon.RelayInfo) (*dto.GeneralOpenAIRequest, error) { - return geminichat.GeminiGenerateContentRequestToOpenAIChat(geminiRequest, info) -} - -func OpenAIChatRequestToGeminiGenerateContent(c *gin.Context, textRequest dto.GeneralOpenAIRequest, info *relaycommon.RelayInfo) (*dto.GeminiChatRequest, error) { - return oaichat.OpenAIChatRequestToGeminiGenerateContent(c, textRequest, info) -} - -func ApplyGeminiThinkingConfig(geminiRequest *dto.GeminiChatRequest, info *relaycommon.RelayInfo, oaiRequest ...dto.GeneralOpenAIRequest) { - sharedgemini.ApplyThinkingConfig(geminiRequest, info, oaiRequest...) -} - -func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*dto.OpenAIResponsesRequest, error) { - return oaichat.ChatCompletionsRequestToResponsesRequest(req) -} - -func ResponsesRequestToChatCompletionsRequest(req *dto.OpenAIResponsesRequest) (*dto.GeneralOpenAIRequest, error) { - return oairesponses.ResponsesRequestToChatCompletionsRequest(req) -} - -func OpenAIResponsesRequestToClaudeMessages(c *gin.Context, req *dto.OpenAIResponsesRequest) (*dto.ClaudeRequest, error) { - return oairesponses.OpenAIResponsesRequestToClaudeMessages(c, req) -} - -func OpenAIResponsesRequestToGeminiChat(c *gin.Context, req *dto.OpenAIResponsesRequest, info *relaycommon.RelayInfo) (*dto.GeminiChatRequest, error) { - return oairesponses.OpenAIResponsesRequestToGeminiChat(c, req, info) -} - -func ShouldChatCompletionsUseResponsesPolicy(policy model_setting.ChatCompletionsToResponsesPolicy, channelID int, channelType int, model string) bool { - return oaichat.ShouldChatCompletionsUseResponsesPolicy(policy, channelID, channelType, model) -} - -func ShouldChatCompletionsUseResponsesGlobal(channelID int, channelType int, model string) bool { - return oaichat.ShouldChatCompletionsUseResponsesGlobal(channelID, channelType, model) -} diff --git a/service/request_converter.go b/service/request_converter.go index 3c912ffe..3b0f6ea7 100644 --- a/service/request_converter.go +++ b/service/request_converter.go @@ -1,18 +1,24 @@ package service import ( + "context" "fmt" - "github.com/QuantumNous/new-api/dto" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/service/relayconvert" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/relayconvert" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) func init() { relayconvert.SetMediaResolver(relayconvert.MediaResolver{ - GetBase64Data: GetBase64Data, + // relayconvert is gin-free; recover the gin context when the caller + // passed one so file caching/cleanup keeps working. + GetBase64Data: func(ctx context.Context, source types.FileSource, reason ...string) (string, string, error) { + ginCtx, _ := ctx.(*gin.Context) + return GetBase64Data(ginCtx, source, reason...) + }, DecodeBase64FileData: DecodeBase64FileData, }) } diff --git a/service/sensitive.go b/service/sensitive.go index 3c780998..ffacbf4f 100644 --- a/service/sensitive.go +++ b/service/sensitive.go @@ -4,7 +4,7 @@ import ( "errors" "strings" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting" ) diff --git a/service/task_billing.go b/service/task_billing.go index b677cb62..62ba1231 100644 --- a/service/task_billing.go +++ b/service/task_billing.go @@ -10,8 +10,8 @@ import ( "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/ratio_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/service/task_billing_test.go b/service/task_billing_test.go index 53e3f680..6a0ae123 100644 --- a/service/task_billing_test.go +++ b/service/task_billing_test.go @@ -12,7 +12,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" relaycommon "github.com/QuantumNous/new-api/relay/common" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/glebarez/sqlite" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" diff --git a/service/task_polling.go b/service/task_polling.go index c6ffeec8..250201ae 100644 --- a/service/task_polling.go +++ b/service/task_polling.go @@ -13,11 +13,12 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + taskdto "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/bytedance/gopkg/util/gopool" "github.com/samber/lo" @@ -255,7 +256,7 @@ func updateSunoTasks(ctx context.Context, channelId int, taskIds []string, taskM common.SysLog(fmt.Sprintf("Get Suno Task parse body error: %v", err)) return err } - var responseItems dto.TaskResponse[[]dto.SunoDataResponse] + var responseItems taskdto.TaskResponse[[]taskdto.SunoDataResponse] err = common.Unmarshal(responseBody, &responseItems) if err != nil { logger.LogError(ctx, fmt.Sprintf("Get Suno Task parse body error2: %v, body: %s", err, string(responseBody))) @@ -310,7 +311,7 @@ func updateSunoTasks(ctx context.Context, channelId int, taskIds []string, taskM } // taskNeedsUpdate 检查 Suno 任务是否需要更新 -func taskNeedsUpdate(oldTask *model.Task, newTask dto.SunoDataResponse) bool { +func taskNeedsUpdate(oldTask *model.Task, newTask taskdto.SunoDataResponse) bool { if oldTask.SubmitTime != newTask.SubmitTime { return true } @@ -477,7 +478,7 @@ func updateVideoSingleTask(ctx context.Context, adaptor TaskPollingAdaptor, ch * taskResult := &relaycommon.TaskInfo{} // try parse as New API response format - var responseItems dto.TaskResponse[model.Task] + var responseItems taskdto.TaskResponse[model.Task] if err = common.Unmarshal(responseBody, &responseItems); err == nil && responseItems.IsSuccess() { logger.LogDebug(ctx, "updateVideoSingleTask parsed as new api response format: %+v", responseItems) t := responseItems.Data diff --git a/service/task_polling_test.go b/service/task_polling_test.go index 228eb40f..57b382fd 100644 --- a/service/task_polling_test.go +++ b/service/task_polling_test.go @@ -11,9 +11,10 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + taskdto "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/bytedance/gopkg/util/gopool" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -37,9 +38,9 @@ func (a *sunoFailurePollingAdaptor) Init(_ *relaycommon.RelayInfo) {} func (a *sunoFailurePollingAdaptor) FetchTask(_ string, _ string, body map[string]any, _ string) (*http.Response, error) { taskIDs, _ := body["ids"].([]string) - items := make([]dto.SunoDataResponse, 0, len(taskIDs)) + items := make([]taskdto.SunoDataResponse, 0, len(taskIDs)) for _, taskID := range taskIDs { - items = append(items, dto.SunoDataResponse{ + items = append(items, taskdto.SunoDataResponse{ TaskID: taskID, Status: string(model.TaskStatusFailure), FailReason: a.failReason, @@ -47,8 +48,8 @@ func (a *sunoFailurePollingAdaptor) FetchTask(_ string, _ string, body map[strin }) } - responseBody, err := common.Marshal(dto.TaskResponse[[]dto.SunoDataResponse]{ - Code: dto.TaskSuccessCode, + responseBody, err := common.Marshal(taskdto.TaskResponse[[]taskdto.SunoDataResponse]{ + Code: taskdto.TaskSuccessCode, Data: items, }) if err != nil { @@ -91,8 +92,8 @@ func (a *taskPollingFetchAdaptor) FetchTask(_ string, _ string, body map[string] } } - response := dto.TaskResponse[model.Task]{ - Code: dto.TaskSuccessCode, + response := taskdto.TaskResponse[model.Task]{ + Code: taskdto.TaskSuccessCode, Data: model.Task{ TaskID: taskID, Status: model.TaskStatusInProgress, diff --git a/service/text_quota.go b/service/text_quota.go index 414a98c1..b7578f73 100644 --- a/service/text_quota.go +++ b/service/text_quota.go @@ -9,15 +9,15 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/pkg/billingexpr" perfmetrics "github.com/QuantumNous/new-api/pkg/perf_metrics" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/types" "github.com/bytedance/gopkg/util/gopool" "github.com/gin-gonic/gin" diff --git a/service/text_quota_test.go b/service/text_quota_test.go index d9e9a6c6..a27df970 100644 --- a/service/text_quota_test.go +++ b/service/text_quota_test.go @@ -8,12 +8,12 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/pkg/billingexpr" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/shopspring/decimal" diff --git a/service/tiered_settle.go b/service/tiered_settle.go index 05337bd7..785ee934 100644 --- a/service/tiered_settle.go +++ b/service/tiered_settle.go @@ -1,9 +1,9 @@ package service import ( - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/pkg/billingexpr" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" ) // TieredResultWrapper wraps billingexpr.TieredResult for use at the service layer. diff --git a/service/tiered_settle_test.go b/service/tiered_settle_test.go index eaf13395..de8abd90 100644 --- a/service/tiered_settle_test.go +++ b/service/tiered_settle_test.go @@ -5,9 +5,9 @@ import ( "math/rand" "testing" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/pkg/billingexpr" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/shopspring/decimal" ) diff --git a/service/token_counter.go b/service/token_counter.go index 6fcc4f1c..aad320a1 100644 --- a/service/token_counter.go +++ b/service/token_counter.go @@ -10,11 +10,11 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" constant2 "github.com/QuantumNous/new-api/relay/constant" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/dto" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/gin-gonic/gin" ) diff --git a/service/usage_helpr.go b/service/usage_helpr.go index 97d54c4f..b5c35ee7 100644 --- a/service/usage_helpr.go +++ b/service/usage_helpr.go @@ -3,7 +3,7 @@ package service import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/gin-gonic/gin" ) diff --git a/service/user_notify.go b/service/user_notify.go index 74a7c6a5..5f015b43 100644 --- a/service/user_notify.go +++ b/service/user_notify.go @@ -8,8 +8,8 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting/system_setting" ) diff --git a/service/violation_fee.go b/service/violation_fee.go index 45508856..f5153362 100644 --- a/service/violation_fee.go +++ b/service/violation_fee.go @@ -9,8 +9,8 @@ import ( "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/model_setting" - "github.com/QuantumNous/new-api/types" "github.com/shopspring/decimal" diff --git a/service/webhook.go b/service/webhook.go index ba294fa2..5d84e7da 100644 --- a/service/webhook.go +++ b/service/webhook.go @@ -10,7 +10,7 @@ import ( "time" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting/system_setting" ) diff --git a/setting/model_setting/claude.go b/setting/model_setting/claude.go index 3173bda2..2ce5d2c6 100644 --- a/setting/model_setting/claude.go +++ b/setting/model_setting/claude.go @@ -1,9 +1,11 @@ package model_setting import ( + "fmt" "net/http" "strings" + "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/setting/config" ) @@ -87,3 +89,23 @@ func (c *ClaudeSettings) GetDefaultMaxTokens(model string) int { } return c.DefaultMaxTokens["default"] } + +// ValidateClaudeDefaultMaxTokens validates the JSON persisted by the option +// API. Zero stays allowed — the current Messages API accepts max_tokens: 0 as +// cache pre-warming — but negative values are rejected because they would +// wrap into huge unsigned values during request conversion. +func ValidateClaudeDefaultMaxTokens(value string) error { + var settings map[string]int + if err := common.UnmarshalJsonStr(value, &settings); err != nil { + return fmt.Errorf("Claude default max tokens must be a JSON map of model to integer: %w", err) + } + if settings == nil { + return fmt.Errorf("Claude default max tokens must be a JSON map of model to integer") + } + for model, maxTokens := range settings { + if maxTokens < 0 { + return fmt.Errorf("negative Claude default max_tokens %d for %q", maxTokens, model) + } + } + return nil +} diff --git a/setting/model_setting/claude_test.go b/setting/model_setting/claude_test.go index 0a806a7a..db62ca99 100644 --- a/setting/model_setting/claude_test.go +++ b/setting/model_setting/claude_test.go @@ -3,6 +3,9 @@ package model_setting import ( "net/http" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestClaudeSettingsWriteHeadersMergesConfiguredValuesIntoSingleHeader(t *testing.T) { @@ -58,3 +61,32 @@ func TestClaudeSettingsWriteHeadersDeduplicatesAcrossCommaSeparatedAndRepeatedVa t.Fatalf("expected deduplicated merged header %q, got %q", expected, got[0]) } } + +func TestValidateClaudeDefaultMaxTokens(t *testing.T) { + tests := []struct { + name string + value string + wantErr string + }{ + {name: "positive default", value: `{"default": 8192}`}, + {name: "zero allowed", value: `{"default": 0}`}, + {name: "zero model override allowed", value: `{"default": 8192, "claude-test": 0}`}, + {name: "empty map allowed", value: `{}`}, + {name: "negative default rejected", value: `{"default": -1}`, wantErr: `negative Claude default max_tokens -1 for "default"`}, + {name: "negative model override rejected", value: `{"default": 8192, "claude-test": -5}`, wantErr: `negative Claude default max_tokens -5 for "claude-test"`}, + {name: "non-integer rejected", value: `{"default": "high"}`, wantErr: "JSON map of model to integer"}, + {name: "null rejected", value: `null`, wantErr: "JSON map of model to integer"}, + {name: "malformed rejected", value: `{`, wantErr: "JSON map of model to integer"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateClaudeDefaultMaxTokens(tt.value) + if tt.wantErr == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + }) + } +} diff --git a/setting/model_setting/gemini.go b/setting/model_setting/gemini.go index ea043c69..8e21cc6f 100644 --- a/setting/model_setting/gemini.go +++ b/setting/model_setting/gemini.go @@ -1,9 +1,23 @@ package model_setting import ( + "fmt" + + "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/setting/config" ) +const defaultGeminiSafetySetting = "OFF" + +var validGeminiSafetySettings = map[string]struct{}{ + "OFF": {}, + "BLOCK_NONE": {}, + "BLOCK_ONLY_HIGH": {}, + "BLOCK_MEDIUM_AND_ABOVE": {}, + "BLOCK_LOW_AND_ABOVE": {}, + "HARM_BLOCK_THRESHOLD_UNSPECIFIED": {}, +} + // GeminiSettings defines Gemini model configuration. 注意bool要以enabled结尾才可以生效编辑 type GeminiSettings struct { SafetySettings map[string]string `json:"safety_settings"` @@ -18,7 +32,7 @@ type GeminiSettings struct { // 默认配置 var defaultGeminiSettings = GeminiSettings{ SafetySettings: map[string]string{ - "default": "OFF", + "default": defaultGeminiSafetySetting, }, VersionSettings: map[string]string{ "default": "v1beta", @@ -54,10 +68,35 @@ func GetGeminiSettings() *GeminiSettings { // GetGeminiSafetySetting 获取安全设置 func GetGeminiSafetySetting(key string) string { - if value, ok := geminiSettings.SafetySettings[key]; ok { + settings := geminiSettings.SafetySettings + if value := settings[key]; value != "" { return value } - return geminiSettings.SafetySettings["default"] + if value := settings["default"]; value != "" { + return value + } + return defaultGeminiSafetySetting +} + +// ValidateGeminiSafetySettings validates the JSON persisted by the option API. +// Empty values remain valid because read-time fallback returns the default. +func ValidateGeminiSafetySettings(value string) error { + var settings map[string]string + if err := common.UnmarshalJsonStr(value, &settings); err != nil { + return fmt.Errorf("Gemini safety settings must be a JSON string map: %w", err) + } + if settings == nil { + return fmt.Errorf("Gemini safety settings must be a JSON string map") + } + for category, threshold := range settings { + if threshold == "" { + continue + } + if _, ok := validGeminiSafetySettings[threshold]; !ok { + return fmt.Errorf("invalid Gemini safety threshold %q for %q", threshold, category) + } + } + return nil } // GetGeminiVersionSetting 获取版本设置 diff --git a/setting/model_setting/gemini_test.go b/setting/model_setting/gemini_test.go new file mode 100644 index 00000000..26529719 --- /dev/null +++ b/setting/model_setting/gemini_test.go @@ -0,0 +1,99 @@ +package model_setting + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGeminiSafetySettingsReadNormalization(t *testing.T) { + original := geminiSettings.SafetySettings + t.Cleanup(func() { + geminiSettings.SafetySettings = original + }) + + tests := []struct { + name string + settings map[string]string + key string + want string + }{ + { + name: "nil map gets OFF default", + settings: nil, + key: "HARM_CATEGORY_HATE_SPEECH", + want: "OFF", + }, + { + name: "missing default gets OFF without replacing existing values", + settings: map[string]string{ + "HARM_CATEGORY_HATE_SPEECH": "BLOCK_SOME", + }, + key: "HARM_CATEGORY_HATE_SPEECH", + want: "BLOCK_SOME", + }, + { + name: "empty default gets OFF", + settings: map[string]string{ + "default": "", + }, + key: "HARM_CATEGORY_HATE_SPEECH", + want: "OFF", + }, + { + name: "empty override falls back to configured default", + settings: map[string]string{ + "default": "BLOCK_ONLY_HIGH", + "HARM_CATEGORY_HATE_SPEECH": "", + }, + key: "HARM_CATEGORY_HATE_SPEECH", + want: "BLOCK_ONLY_HIGH", + }, + { + name: "historical invalid nonempty default is preserved", + settings: map[string]string{ + "default": "BLOCK_SOME", + }, + key: "HARM_CATEGORY_HATE_SPEECH", + want: "BLOCK_SOME", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + geminiSettings.SafetySettings = test.settings + + assert.Equal(t, test.want, GetGeminiSafetySetting(test.key)) + }) + } +} + +func TestValidateGeminiSafetySettings(t *testing.T) { + valid := []string{ + `{}`, + `{"default":""}`, + `{"HARM_CATEGORY_HATE_SPEECH":""}`, + `{"default":"OFF"}`, + `{"default":"BLOCK_NONE"}`, + `{"default":"BLOCK_ONLY_HIGH"}`, + `{"default":"BLOCK_MEDIUM_AND_ABOVE"}`, + `{"default":"BLOCK_LOW_AND_ABOVE"}`, + `{"default":"HARM_BLOCK_THRESHOLD_UNSPECIFIED"}`, + } + for _, value := range valid { + require.NoError(t, ValidateGeminiSafetySettings(value), value) + } + + invalid := []string{ + `null`, + `[]`, + `{"default":1}`, + `{"default":"BLOCK_SOME"}`, + `{"default":" off "}`, + `{"default":`, + } + for _, value := range invalid { + assert.Error(t, ValidateGeminiSafetySettings(value), value) + } +} diff --git a/setting/operation_setting/status_code_ranges.go b/setting/operation_setting/status_code_ranges.go index 14cfacad..7c61afa0 100644 --- a/setting/operation_setting/status_code_ranges.go +++ b/setting/operation_setting/status_code_ranges.go @@ -6,7 +6,7 @@ import ( "strconv" "strings" - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" ) type StatusCodeRange struct { diff --git a/setting/payment_waffo.go b/setting/payment_waffo.go index c27ca6f2..ad6f5fb2 100644 --- a/setting/payment_waffo.go +++ b/setting/payment_waffo.go @@ -6,21 +6,21 @@ import ( ) var ( - WaffoEnabled bool - WaffoApiKey string - WaffoPrivateKey string - WaffoPublicCert string - WaffoSandboxPublicCert string - WaffoSandboxApiKey string - WaffoSandboxPrivateKey string - WaffoSandbox bool - WaffoMerchantId string + WaffoEnabled bool + WaffoApiKey string + WaffoPrivateKey string + WaffoPublicCert string + WaffoSandboxPublicCert string + WaffoSandboxApiKey string + WaffoSandboxPrivateKey string + WaffoSandbox bool + WaffoMerchantId string WaffoNotifyUrl string WaffoReturnUrl string WaffoSubscriptionReturnUrl string - WaffoCurrency string - WaffoUnitPrice float64 = 1.0 - WaffoMinTopUp int = 1 + WaffoCurrency string + WaffoUnitPrice float64 = 1.0 + WaffoMinTopUp int = 1 ) // GetWaffoPayMethods 从 options 读取 Waffo 支付方式配置 diff --git a/setting/ratio_setting/cache_ratio.go b/setting/ratio_setting/cache_ratio.go index 6e874b5b..42147607 100644 --- a/setting/ratio_setting/cache_ratio.go +++ b/setting/ratio_setting/cache_ratio.go @@ -1,7 +1,7 @@ package ratio_setting import ( - "github.com/QuantumNous/new-api/types" + "github.com/QuantumNous/new-api/relaykit/types" ) var defaultCacheRatio = map[string]float64{ diff --git a/setting/ratio_setting/group_ratio.go b/setting/ratio_setting/group_ratio.go index 7d16d928..c52f5e5e 100644 --- a/setting/ratio_setting/group_ratio.go +++ b/setting/ratio_setting/group_ratio.go @@ -5,8 +5,8 @@ import ( "errors" "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/config" - "github.com/QuantumNous/new-api/types" ) var defaultGroupRatio = map[string]float64{ diff --git a/setting/ratio_setting/model_ratio.go b/setting/ratio_setting/model_ratio.go index 829e0794..e3ecc344 100644 --- a/setting/ratio_setting/model_ratio.go +++ b/setting/ratio_setting/model_ratio.go @@ -4,8 +4,8 @@ import ( "strings" "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/types" ) // from songquanpeng/one-api diff --git a/setting/reasoning/suffix.go b/setting/reasoning/suffix.go index 59140a7c..fd93546a 100644 --- a/setting/reasoning/suffix.go +++ b/setting/reasoning/suffix.go @@ -1,51 +1,19 @@ +// Package reasoning re-exports the pure model-name effort-suffix helpers, +// which moved to the conversion kit (service/relayconvert/reasoning) as part +// of the relaykit extraction. Host code keeps importing this path unchanged. package reasoning -import ( - "strings" +import kitreasoning "github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning" - "github.com/samber/lo" +var ( + EffortSuffixes = kitreasoning.EffortSuffixes + OpenAIEffortSuffixes = kitreasoning.OpenAIEffortSuffixes + DeepSeekV4EffortSuffixes = kitreasoning.DeepSeekV4EffortSuffixes ) -var EffortSuffixes = []string{"-max", "-xhigh", "-high", "-medium", "-low", "-minimal"} - -var OpenAIEffortSuffixes = []string{"-high", "-minimal", "-low", "-medium", "-none", "-xhigh"} - -var DeepSeekV4EffortSuffixes = []string{"-none", "-max"} - -// TrimEffortSuffix -> modelName level(low) exists -func TrimEffortSuffix(modelName string) (string, string, bool) { - return TrimEffortSuffixWithSuffixes(modelName, EffortSuffixes) -} - -func TrimEffortSuffixWithSuffixes(modelName string, suffixes []string) (string, string, bool) { - suffix, found := lo.Find(suffixes, func(s string) bool { - return strings.HasSuffix(modelName, s) - }) - if !found { - return modelName, "", false - } - return strings.TrimSuffix(modelName, suffix), strings.TrimPrefix(suffix, "-"), true -} - -func ParseOpenAIReasoningEffortFromModelSuffix(modelName string) (string, string) { - baseModel, effort, ok := TrimEffortSuffixWithSuffixes(modelName, OpenAIEffortSuffixes) - if !ok { - return "", modelName - } - return effort, baseModel -} - -func ParseDeepSeekV4ThinkingSuffix(modelName string) (baseModel string, thinkingType string, effort string, ok bool) { - baseModel, suffix, ok := TrimEffortSuffixWithSuffixes(modelName, DeepSeekV4EffortSuffixes) - if !ok || !strings.HasPrefix(baseModel, "deepseek-v4-") { - return modelName, "", "", false - } - switch suffix { - case "none": - return baseModel, "disabled", "", true - case "max": - return baseModel, "enabled", "max", true - default: - return modelName, "", "", false - } -} +var ( + TrimEffortSuffix = kitreasoning.TrimEffortSuffix + TrimEffortSuffixWithSuffixes = kitreasoning.TrimEffortSuffixWithSuffixes + ParseOpenAIReasoningEffortFromModelSuffix = kitreasoning.ParseOpenAIReasoningEffortFromModelSuffix + ParseDeepSeekV4ThinkingSuffix = kitreasoning.ParseDeepSeekV4ThinkingSuffix +)