langchain-githubcopilot-chat 0.4.0__tar.gz → 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {langchain_githubcopilot_chat-0.4.0 → langchain_githubcopilot_chat-0.5.1}/PKG-INFO +1 -1
- {langchain_githubcopilot_chat-0.4.0 → langchain_githubcopilot_chat-0.5.1}/langchain_githubcopilot_chat/auth.py +13 -4
- {langchain_githubcopilot_chat-0.4.0 → langchain_githubcopilot_chat-0.5.1}/langchain_githubcopilot_chat/chat_models.py +155 -110
- {langchain_githubcopilot_chat-0.4.0 → langchain_githubcopilot_chat-0.5.1}/langchain_githubcopilot_chat/embeddings.py +9 -0
- {langchain_githubcopilot_chat-0.4.0 → langchain_githubcopilot_chat-0.5.1}/pyproject.toml +1 -1
- {langchain_githubcopilot_chat-0.4.0 → langchain_githubcopilot_chat-0.5.1}/LICENSE +0 -0
- {langchain_githubcopilot_chat-0.4.0 → langchain_githubcopilot_chat-0.5.1}/LICENSE.langchain +0 -0
- {langchain_githubcopilot_chat-0.4.0 → langchain_githubcopilot_chat-0.5.1}/README.md +0 -0
- {langchain_githubcopilot_chat-0.4.0 → langchain_githubcopilot_chat-0.5.1}/langchain_githubcopilot_chat/__init__.py +0 -0
- {langchain_githubcopilot_chat-0.4.0 → langchain_githubcopilot_chat-0.5.1}/langchain_githubcopilot_chat/py.typed +0 -0
|
@@ -4,12 +4,16 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json
|
|
7
|
+
import logging
|
|
7
8
|
import os
|
|
9
|
+
import threading
|
|
8
10
|
import time
|
|
9
11
|
from typing import Callable, Dict, Optional, Tuple, Union
|
|
10
12
|
|
|
11
13
|
import httpx
|
|
12
14
|
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
13
17
|
CLIENT_ID = "Iv1.b507a08c87ecfe98"
|
|
14
18
|
CACHE_PATH = os.path.expanduser("~/.github-copilot-chat.json")
|
|
15
19
|
|
|
@@ -31,7 +35,7 @@ COPILOT_DEFAULT_HEADERS = {
|
|
|
31
35
|
|
|
32
36
|
# In-memory lock for token refresh to prevent concurrent refresh attempts
|
|
33
37
|
_token_refresh_lock: Optional[asyncio.Lock] = None
|
|
34
|
-
_sync_token_refresh_lock:
|
|
38
|
+
_sync_token_refresh_lock: threading.Lock = threading.Lock()
|
|
35
39
|
|
|
36
40
|
|
|
37
41
|
def _get_token_refresh_lock() -> asyncio.Lock:
|
|
@@ -59,8 +63,8 @@ def save_tokens_to_cache(
|
|
|
59
63
|
f,
|
|
60
64
|
indent=2,
|
|
61
65
|
)
|
|
62
|
-
except
|
|
63
|
-
|
|
66
|
+
except OSError as exc:
|
|
67
|
+
logger.warning("Failed to save Copilot token cache to %s: %s", CACHE_PATH, exc)
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
def load_tokens_from_cache() -> Dict[str, str]:
|
|
@@ -74,7 +78,12 @@ def load_tokens_from_cache() -> Dict[str, str]:
|
|
|
74
78
|
# Token expired, return empty
|
|
75
79
|
return {}
|
|
76
80
|
return data
|
|
77
|
-
except
|
|
81
|
+
except FileNotFoundError:
|
|
82
|
+
return {} # cache doesn't exist yet — silently OK
|
|
83
|
+
except (OSError, json.JSONDecodeError, KeyError, ValueError) as exc:
|
|
84
|
+
logger.warning(
|
|
85
|
+
"Failed to load Copilot token cache from %s: %s", CACHE_PATH, exc
|
|
86
|
+
)
|
|
78
87
|
return {}
|
|
79
88
|
|
|
80
89
|
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import json
|
|
7
|
+
import logging
|
|
6
8
|
import os
|
|
9
|
+
import random
|
|
10
|
+
import time
|
|
7
11
|
from typing import (
|
|
8
12
|
Any,
|
|
9
13
|
AsyncIterator,
|
|
@@ -46,17 +50,19 @@ from pydantic import Field, PrivateAttr, SecretStr, model_validator
|
|
|
46
50
|
|
|
47
51
|
from langchain_githubcopilot_chat.auth import (
|
|
48
52
|
COPILOT_DEFAULT_HEADERS,
|
|
49
|
-
COPILOT_EDITOR_VERSION,
|
|
50
|
-
COPILOT_INTEGRATION_ID,
|
|
51
|
-
COPILOT_PLUGIN_VERSION,
|
|
52
|
-
COPILOT_USER_AGENT,
|
|
53
53
|
_get_token_refresh_lock,
|
|
54
|
+
_sync_token_refresh_lock,
|
|
54
55
|
afetch_copilot_token,
|
|
55
56
|
fetch_copilot_token,
|
|
56
57
|
load_tokens_from_cache,
|
|
57
58
|
save_tokens_to_cache,
|
|
58
59
|
)
|
|
59
60
|
|
|
61
|
+
logger = logging.getLogger(__name__)
|
|
62
|
+
|
|
63
|
+
# Buffer (seconds) before token expiry to trigger a proactive refresh
|
|
64
|
+
_TOKEN_REFRESH_BUFFER_SECS: int = 60
|
|
65
|
+
|
|
60
66
|
# ---------------------------------------------------------------------------
|
|
61
67
|
# Helpers
|
|
62
68
|
# ---------------------------------------------------------------------------
|
|
@@ -204,37 +210,11 @@ def _build_ai_message(
|
|
|
204
210
|
|
|
205
211
|
usage_metadata: Optional[UsageMetadata] = None
|
|
206
212
|
if usage:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
]
|
|
213
|
-
|
|
214
|
-
output_token_details: Dict[str, Any] = {}
|
|
215
|
-
if "reasoning_tokens" in usage:
|
|
216
|
-
output_token_details["reasoning"] = usage["reasoning_tokens"]
|
|
217
|
-
if "completion_tokens_details" in usage:
|
|
218
|
-
if "accepted_prediction_tokens" in usage["completion_tokens_details"]:
|
|
219
|
-
output_token_details["accepted_prediction"] = usage[
|
|
220
|
-
"completion_tokens_details"
|
|
221
|
-
]["accepted_prediction_tokens"]
|
|
222
|
-
if "rejected_prediction_tokens" in usage["completion_tokens_details"]:
|
|
223
|
-
output_token_details["rejected_prediction"] = usage[
|
|
224
|
-
"completion_tokens_details"
|
|
225
|
-
]["rejected_prediction_tokens"]
|
|
226
|
-
|
|
227
|
-
kwargs = {
|
|
228
|
-
"input_tokens": usage.get("prompt_tokens", 0),
|
|
229
|
-
"output_tokens": usage.get("completion_tokens", 0),
|
|
230
|
-
"total_tokens": usage.get("total_tokens", 0),
|
|
231
|
-
}
|
|
232
|
-
if input_token_details:
|
|
233
|
-
kwargs["input_token_details"] = input_token_details
|
|
234
|
-
if output_token_details:
|
|
235
|
-
kwargs["output_token_details"] = output_token_details
|
|
236
|
-
|
|
237
|
-
usage_metadata = UsageMetadata(**kwargs)
|
|
213
|
+
usage_metadata = UsageMetadata(
|
|
214
|
+
input_tokens=usage.get("prompt_tokens", 0),
|
|
215
|
+
output_tokens=usage.get("completion_tokens", 0),
|
|
216
|
+
total_tokens=usage.get("total_tokens", 0),
|
|
217
|
+
)
|
|
238
218
|
|
|
239
219
|
response_metadata: Dict[str, Any] = {
|
|
240
220
|
"finish_reason": finish_reason,
|
|
@@ -480,6 +460,7 @@ class ChatGithubCopilot(BaseChatModel):
|
|
|
480
460
|
# ------------------------------------------------------------------
|
|
481
461
|
|
|
482
462
|
_cached_copilot_token: Optional[str] = PrivateAttr(default=None)
|
|
463
|
+
_cached_copilot_token_expires_at: Optional[float] = PrivateAttr(default=None)
|
|
483
464
|
|
|
484
465
|
@model_validator(mode="before")
|
|
485
466
|
@classmethod
|
|
@@ -510,8 +491,19 @@ class ChatGithubCopilot(BaseChatModel):
|
|
|
510
491
|
@property
|
|
511
492
|
def _token(self) -> str:
|
|
512
493
|
"""Return the raw GitHub token string."""
|
|
513
|
-
|
|
514
|
-
|
|
494
|
+
# Use getattr to avoid triggering Pydantic's __getattr__ on PrivateAttr
|
|
495
|
+
# when instance is created via __new__ without proper initialization
|
|
496
|
+
cached = getattr(self, "_cached_copilot_token", None)
|
|
497
|
+
cached_exp = getattr(self, "_cached_copilot_token_expires_at", None)
|
|
498
|
+
if cached:
|
|
499
|
+
expires_ok = cached_exp is None or (
|
|
500
|
+
time.time() < cached_exp - _TOKEN_REFRESH_BUFFER_SECS
|
|
501
|
+
)
|
|
502
|
+
if expires_ok:
|
|
503
|
+
return cached
|
|
504
|
+
# Token is expired or within the refresh buffer — clear and refresh
|
|
505
|
+
self._cached_copilot_token = None
|
|
506
|
+
self._cached_copilot_token_expires_at = None
|
|
515
507
|
|
|
516
508
|
token = None
|
|
517
509
|
if self.github_token:
|
|
@@ -522,6 +514,10 @@ class ChatGithubCopilot(BaseChatModel):
|
|
|
522
514
|
tokens = load_tokens_from_cache()
|
|
523
515
|
if "copilot_token" in tokens:
|
|
524
516
|
self._cached_copilot_token = tokens["copilot_token"]
|
|
517
|
+
raw_exp = tokens.get("expires_at")
|
|
518
|
+
self._cached_copilot_token_expires_at = (
|
|
519
|
+
float(raw_exp) if raw_exp is not None else None
|
|
520
|
+
)
|
|
525
521
|
return tokens["copilot_token"]
|
|
526
522
|
elif "github_token" in tokens:
|
|
527
523
|
token = tokens["github_token"]
|
|
@@ -533,20 +529,28 @@ class ChatGithubCopilot(BaseChatModel):
|
|
|
533
529
|
"to authenticate."
|
|
534
530
|
)
|
|
535
531
|
|
|
536
|
-
# If the token is a standard GitHub token, exchange it
|
|
532
|
+
# If the token is a standard GitHub token, try to exchange it
|
|
533
|
+
# for a Copilot token. This may fail in environments without
|
|
534
|
+
# network access (e.g., CI), so we catch exceptions.
|
|
537
535
|
if token.startswith(("gho_", "ghp_", "ghu_")):
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
536
|
+
try:
|
|
537
|
+
self._refresh_token_sync(token)
|
|
538
|
+
cached = getattr(self, "_cached_copilot_token", None)
|
|
539
|
+
if cached:
|
|
540
|
+
return cached
|
|
541
|
+
except Exception as exc:
|
|
542
|
+
# Network unavailable, socket blocked, or other transient error.
|
|
543
|
+
# Fall back to using the raw GitHub token directly.
|
|
544
|
+
logger.debug(
|
|
545
|
+
"Token exchange failed (will use raw GitHub token): %s", exc
|
|
546
|
+
)
|
|
541
547
|
|
|
542
548
|
return token
|
|
543
549
|
|
|
544
550
|
def _refresh_token_sync(self, github_token: Optional[str] = None) -> None:
|
|
545
|
-
#
|
|
546
|
-
|
|
547
|
-
if _sync_token_refresh_lock:
|
|
551
|
+
# Non-blocking acquire: if another thread is already refreshing, skip
|
|
552
|
+
if not _sync_token_refresh_lock.acquire(blocking=False):
|
|
548
553
|
return
|
|
549
|
-
_sync_token_refresh_lock = True
|
|
550
554
|
try:
|
|
551
555
|
token_to_use = github_token or (
|
|
552
556
|
self.github_token.get_secret_value() if self.github_token else None
|
|
@@ -559,9 +563,10 @@ class ChatGithubCopilot(BaseChatModel):
|
|
|
559
563
|
new_token, expires_at = fetch_copilot_token(token_to_use)
|
|
560
564
|
if new_token:
|
|
561
565
|
self._cached_copilot_token = new_token
|
|
566
|
+
self._cached_copilot_token_expires_at = expires_at
|
|
562
567
|
save_tokens_to_cache(token_to_use, new_token, expires_at)
|
|
563
568
|
finally:
|
|
564
|
-
_sync_token_refresh_lock
|
|
569
|
+
_sync_token_refresh_lock.release()
|
|
565
570
|
|
|
566
571
|
async def _refresh_token_async(self, github_token: Optional[str] = None) -> None:
|
|
567
572
|
lock = _get_token_refresh_lock()
|
|
@@ -577,6 +582,7 @@ class ChatGithubCopilot(BaseChatModel):
|
|
|
577
582
|
new_token, expires_at = await afetch_copilot_token(token_to_use)
|
|
578
583
|
if new_token:
|
|
579
584
|
self._cached_copilot_token = new_token
|
|
585
|
+
self._cached_copilot_token_expires_at = expires_at
|
|
580
586
|
save_tokens_to_cache(token_to_use, new_token, expires_at)
|
|
581
587
|
|
|
582
588
|
@property
|
|
@@ -673,8 +679,6 @@ class ChatGithubCopilot(BaseChatModel):
|
|
|
673
679
|
|
|
674
680
|
def _do_request(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
675
681
|
"""Perform a synchronous (non-streaming) HTTP POST with retries."""
|
|
676
|
-
import time
|
|
677
|
-
|
|
678
682
|
headers = self._build_headers()
|
|
679
683
|
last_exc: Optional[Exception] = None
|
|
680
684
|
for attempt in range(self.max_retries + 1):
|
|
@@ -710,35 +714,56 @@ class ChatGithubCopilot(BaseChatModel):
|
|
|
710
714
|
if attempt == self.max_retries:
|
|
711
715
|
raise
|
|
712
716
|
if attempt < self.max_retries:
|
|
713
|
-
|
|
717
|
+
backoff = 2**attempt
|
|
718
|
+
time.sleep(backoff + random.uniform(0, backoff * 0.25))
|
|
714
719
|
raise RuntimeError("Unexpected retry loop exit") from last_exc
|
|
715
720
|
|
|
716
721
|
def _do_stream(self, payload: Dict[str, Any]) -> Iterator[Dict[str, Any]]:
|
|
717
722
|
"""Perform a synchronous streaming HTTP POST and yield parsed SSE chunks."""
|
|
718
723
|
headers = self._build_headers()
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
724
|
+
last_exc: Optional[Exception] = None
|
|
725
|
+
for attempt in range(self.max_retries + 1):
|
|
726
|
+
try:
|
|
727
|
+
with httpx.stream(
|
|
728
|
+
"POST",
|
|
729
|
+
self._inference_url,
|
|
730
|
+
headers=headers,
|
|
731
|
+
json=payload,
|
|
732
|
+
timeout=self.timeout,
|
|
733
|
+
) as response:
|
|
734
|
+
if response.status_code == 401:
|
|
735
|
+
self._refresh_token_sync()
|
|
736
|
+
headers = self._build_headers()
|
|
737
|
+
raise httpx.TransportError("401 — token refreshed, retrying")
|
|
738
|
+
response.raise_for_status()
|
|
739
|
+
for line in response.iter_lines():
|
|
740
|
+
line = line.strip()
|
|
741
|
+
if not line or line == "data: [DONE]":
|
|
742
|
+
continue
|
|
743
|
+
if line.startswith("data: "):
|
|
744
|
+
line = line[len("data: ") :]
|
|
745
|
+
try:
|
|
746
|
+
yield json.loads(line)
|
|
747
|
+
except json.JSONDecodeError:
|
|
748
|
+
continue
|
|
749
|
+
return
|
|
750
|
+
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
|
751
|
+
last_exc = exc
|
|
752
|
+
if attempt == self.max_retries:
|
|
753
|
+
raise
|
|
754
|
+
except httpx.HTTPStatusError as exc:
|
|
755
|
+
if exc.response.status_code < 500:
|
|
756
|
+
raise
|
|
757
|
+
last_exc = exc
|
|
758
|
+
if attempt == self.max_retries:
|
|
759
|
+
raise
|
|
760
|
+
if attempt < self.max_retries:
|
|
761
|
+
backoff = 2**attempt
|
|
762
|
+
time.sleep(backoff + random.uniform(0, backoff * 0.25))
|
|
763
|
+
raise RuntimeError("Unexpected retry loop exit") from last_exc
|
|
737
764
|
|
|
738
765
|
async def _do_request_async(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
739
766
|
"""Perform an asynchronous (non-streaming) HTTP POST with retries."""
|
|
740
|
-
import asyncio
|
|
741
|
-
|
|
742
767
|
headers = self._build_headers()
|
|
743
768
|
last_exc: Optional[Exception] = None
|
|
744
769
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
@@ -772,7 +797,8 @@ class ChatGithubCopilot(BaseChatModel):
|
|
|
772
797
|
if attempt == self.max_retries:
|
|
773
798
|
raise
|
|
774
799
|
if attempt < self.max_retries:
|
|
775
|
-
|
|
800
|
+
backoff = 2**attempt
|
|
801
|
+
await asyncio.sleep(backoff + random.uniform(0, backoff * 0.25))
|
|
776
802
|
raise RuntimeError("Unexpected retry loop exit") from last_exc
|
|
777
803
|
|
|
778
804
|
async def _do_stream_async(
|
|
@@ -780,24 +806,48 @@ class ChatGithubCopilot(BaseChatModel):
|
|
|
780
806
|
) -> AsyncIterator[Dict[str, Any]]:
|
|
781
807
|
"""Perform an asynchronous streaming HTTP POST and yield parsed SSE chunks."""
|
|
782
808
|
headers = self._build_headers()
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
self.
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
809
|
+
last_exc: Optional[Exception] = None
|
|
810
|
+
for attempt in range(self.max_retries + 1):
|
|
811
|
+
try:
|
|
812
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
813
|
+
async with client.stream(
|
|
814
|
+
"POST",
|
|
815
|
+
self._inference_url,
|
|
816
|
+
headers=headers,
|
|
817
|
+
json=payload,
|
|
818
|
+
) as response:
|
|
819
|
+
if response.status_code == 401:
|
|
820
|
+
await self._refresh_token_async()
|
|
821
|
+
headers = self._build_headers()
|
|
822
|
+
raise httpx.TransportError(
|
|
823
|
+
"401 — token refreshed, retrying"
|
|
824
|
+
)
|
|
825
|
+
response.raise_for_status()
|
|
826
|
+
async for line in response.aiter_lines():
|
|
827
|
+
line = line.strip()
|
|
828
|
+
if not line or line == "data: [DONE]":
|
|
829
|
+
continue
|
|
830
|
+
if line.startswith("data: "):
|
|
831
|
+
line = line[len("data: ") :]
|
|
832
|
+
try:
|
|
833
|
+
yield json.loads(line)
|
|
834
|
+
except json.JSONDecodeError:
|
|
835
|
+
continue
|
|
836
|
+
return
|
|
837
|
+
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
|
838
|
+
last_exc = exc
|
|
839
|
+
if attempt == self.max_retries:
|
|
840
|
+
raise
|
|
841
|
+
except httpx.HTTPStatusError as exc:
|
|
842
|
+
if exc.response.status_code < 500:
|
|
843
|
+
raise
|
|
844
|
+
last_exc = exc
|
|
845
|
+
if attempt == self.max_retries:
|
|
846
|
+
raise
|
|
847
|
+
if attempt < self.max_retries:
|
|
848
|
+
backoff = 2**attempt
|
|
849
|
+
await asyncio.sleep(backoff + random.uniform(0, backoff * 0.25))
|
|
850
|
+
raise RuntimeError("Unexpected retry loop exit") from last_exc
|
|
801
851
|
|
|
802
852
|
# ------------------------------------------------------------------
|
|
803
853
|
# Stream delta → AIMessageChunk helpers
|
|
@@ -850,6 +900,21 @@ class ChatGithubCopilot(BaseChatModel):
|
|
|
850
900
|
usage_metadata=usage_metadata,
|
|
851
901
|
)
|
|
852
902
|
|
|
903
|
+
@staticmethod
|
|
904
|
+
def _make_usage_chunk(usage: Dict[str, Any]) -> ChatGenerationChunk:
|
|
905
|
+
"""Build a usage-only final ``ChatGenerationChunk`` from a usage dict."""
|
|
906
|
+
return ChatGenerationChunk(
|
|
907
|
+
message=AIMessageChunk(
|
|
908
|
+
content="",
|
|
909
|
+
usage_metadata=UsageMetadata(
|
|
910
|
+
input_tokens=usage.get("prompt_tokens", 0),
|
|
911
|
+
output_tokens=usage.get("completion_tokens", 0),
|
|
912
|
+
total_tokens=usage.get("total_tokens", 0),
|
|
913
|
+
),
|
|
914
|
+
response_metadata={"usage": usage},
|
|
915
|
+
)
|
|
916
|
+
)
|
|
917
|
+
|
|
853
918
|
# ------------------------------------------------------------------
|
|
854
919
|
# LangChain BaseChatModel interface
|
|
855
920
|
# ------------------------------------------------------------------
|
|
@@ -924,17 +989,7 @@ class ChatGithubCopilot(BaseChatModel):
|
|
|
924
989
|
|
|
925
990
|
if not choices and usage:
|
|
926
991
|
# Final usage-only chunk
|
|
927
|
-
chunk =
|
|
928
|
-
message=AIMessageChunk(
|
|
929
|
-
content="",
|
|
930
|
-
usage_metadata=UsageMetadata(
|
|
931
|
-
input_tokens=usage.get("prompt_tokens", 0),
|
|
932
|
-
output_tokens=usage.get("completion_tokens", 0),
|
|
933
|
-
total_tokens=usage.get("total_tokens", 0),
|
|
934
|
-
),
|
|
935
|
-
response_metadata={"usage": usage},
|
|
936
|
-
)
|
|
937
|
-
)
|
|
992
|
+
chunk = self._make_usage_chunk(usage)
|
|
938
993
|
if run_manager:
|
|
939
994
|
run_manager.on_llm_new_token("", chunk=chunk)
|
|
940
995
|
yield chunk
|
|
@@ -990,17 +1045,7 @@ class ChatGithubCopilot(BaseChatModel):
|
|
|
990
1045
|
usage = raw_chunk.get("usage")
|
|
991
1046
|
|
|
992
1047
|
if not choices and usage:
|
|
993
|
-
chunk =
|
|
994
|
-
message=AIMessageChunk(
|
|
995
|
-
content="",
|
|
996
|
-
usage_metadata=UsageMetadata(
|
|
997
|
-
input_tokens=usage.get("prompt_tokens", 0),
|
|
998
|
-
output_tokens=usage.get("completion_tokens", 0),
|
|
999
|
-
total_tokens=usage.get("total_tokens", 0),
|
|
1000
|
-
),
|
|
1001
|
-
response_metadata={"usage": usage},
|
|
1002
|
-
)
|
|
1003
|
-
)
|
|
1048
|
+
chunk = self._make_usage_chunk(usage)
|
|
1004
1049
|
if run_manager:
|
|
1005
1050
|
await run_manager.on_llm_new_token("", chunk=chunk)
|
|
1006
1051
|
yield chunk
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import os
|
|
7
|
+
import random
|
|
8
|
+
import time
|
|
6
9
|
from typing import Any, Dict, List, Optional, Union
|
|
7
10
|
|
|
8
11
|
import httpx
|
|
@@ -211,6 +214,9 @@ class GithubcopilotChatEmbeddings(BaseModel, Embeddings):
|
|
|
211
214
|
last_exc = exc
|
|
212
215
|
if attempt == self.max_retries:
|
|
213
216
|
raise
|
|
217
|
+
if attempt < self.max_retries:
|
|
218
|
+
backoff = 2**attempt
|
|
219
|
+
time.sleep(backoff + random.uniform(0, backoff * 0.25))
|
|
214
220
|
raise RuntimeError("Unexpected retry loop exit") from last_exc
|
|
215
221
|
|
|
216
222
|
async def _do_request_async(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -237,6 +243,9 @@ class GithubcopilotChatEmbeddings(BaseModel, Embeddings):
|
|
|
237
243
|
last_exc = exc
|
|
238
244
|
if attempt == self.max_retries:
|
|
239
245
|
raise
|
|
246
|
+
if attempt < self.max_retries:
|
|
247
|
+
backoff = 2**attempt
|
|
248
|
+
await asyncio.sleep(backoff + random.uniform(0, backoff * 0.25))
|
|
240
249
|
raise RuntimeError("Unexpected retry loop exit") from last_exc
|
|
241
250
|
|
|
242
251
|
@staticmethod
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "langchain-githubcopilot-chat"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.1"
|
|
8
8
|
description = "An integration package connecting GithubcopilotChat and LangChain"
|
|
9
9
|
authors = ["YIhan Wu <iumm@ibat.ac.cn>"]
|
|
10
10
|
readme = "README.md"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|