File size: 3,852 Bytes
0602b26
 
 
9fb95ba
0602b26
 
 
 
 
 
 
4294178
 
9fb95ba
 
4294178
9fb95ba
 
 
 
 
2c96ecb
9fb95ba
0602b26
 
 
49fa921
0602b26
 
 
 
 
 
 
 
 
d1b56bd
 
 
 
 
9fb95ba
 
 
 
 
 
 
 
 
 
 
 
 
 
49fa921
9fb95ba
0602b26
 
4294178
0602b26
9fb95ba
0602b26
 
 
 
d29db6e
0602b26
 
4294178
 
9fb95ba
 
 
 
 
 
 
 
 
 
2c96ecb
 
 
 
 
 
 
 
 
 
 
 
4294178
 
 
 
 
 
2c96ecb
4294178
 
 
 
2c96ecb
4294178
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
"""Single production composition root for the FCC server."""

import os
from functools import partial
from pathlib import Path

from free_claude_code.api.app import create_app
from free_claude_code.api.ports import ApiServices
from free_claude_code.config.logging_config import configure_logging
from free_claude_code.config.paths import server_log_path
from free_claude_code.config.settings import Settings
from free_claude_code.messaging.transcription import TranscriptionService
from free_claude_code.messaging.voice import Transcriber
from free_claude_code.providers.admission import ProviderAdmissionController
from free_claude_code.providers.base import BaseProvider, ProviderConfig
from free_claude_code.providers.nvidia_nim.voice import NvidiaNimTranscriber
from free_claude_code.providers.openai_codex import (
    OpenAIAuthManager,
    OpenAICodexProvider,
)
from free_claude_code.providers.runtime import ProviderRuntime
from free_claude_code.providers.runtime.config import split_api_key_pool
from free_claude_code.providers.runtime.factory import create_provider

from .application import ApplicationRuntime, RestartCallback
from .asgi import RuntimeASGIApp
from .codex_catalog import CodexModelCatalogPublisher
from .provider_manager import ProviderRuntimeManager


def build_asgi_app(
    settings: Settings,
    restart_callback: RestartCallback | None = None,
) -> RuntimeASGIApp:
    """Construct the complete server application and its resource owner."""
    log_path = Path(os.getenv("LOG_FILE", server_log_path()))
    configure_logging(
        log_path,
        level=settings.log_level,
        verbose_third_party=settings.log_raw_api_payloads,
    )
    openai_auth = OpenAIAuthManager(proxy=settings.openai_proxy)
    openai_factory = partial(_create_openai_provider, auth=openai_auth)
    provider_constructor = partial(
        create_provider,
        injected_factories={"openai": openai_factory},
    )
    runtime_factory = partial(
        ProviderRuntime,
        provider_constructor=provider_constructor,
    )
    provider_manager = ProviderRuntimeManager(
        settings,
        runtime_factory=runtime_factory,
        connected_provider_ids=openai_auth.connected_provider_ids,
        model_catalog_publisher=CodexModelCatalogPublisher(),
    )
    runtime = ApplicationRuntime(
        provider_manager,
        transcriber=_create_transcriber(settings),
        restart_callback=restart_callback,
        connected_accounts={"openai": openai_auth},
    )
    services = ApiServices(
        requests=provider_manager,
        admin=runtime,
        tasks=runtime,
    )
    return RuntimeASGIApp(create_app(services), runtime)


def _create_openai_provider(
    config: ProviderConfig,
    _settings: Settings,
    admission: ProviderAdmissionController,
    *,
    auth: OpenAIAuthManager,
) -> BaseProvider:
    return OpenAICodexProvider(config, auth=auth, admission=admission)


def _first_pool_key(value: str) -> str:
    """Return the first key of a comma-separated credential pool.

    Provider chat credentials may be configured as a comma-separated pool for
    round-robin rotation, but the voice transcribers consume a single key and
    must never send the raw joined pool as one token.
    """

    keys = split_api_key_pool(value)
    return keys[0] if keys else ""


def _create_transcriber(settings: Settings) -> Transcriber | None:
    if not settings.voice_note_enabled:
        return None
    if settings.whisper_device == "nvidia_nim":
        return NvidiaNimTranscriber(
            model=settings.whisper_model,
            api_key=_first_pool_key(settings.nvidia_nim_api_key),
        )
    return TranscriptionService(
        model=settings.whisper_model,
        device=settings.whisper_device,
        huggingface_api_key=_first_pool_key(settings.huggingface_api_key),
    )